/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const WSL_0XFFFFFFFF: number = 0xffffffff;
const WSL_0XFF: number = 0xff;
const WSL_0XFE: number = 0xfe;
const WSLNEGATIVE_2: number = -2;
const WSLNEGATIVE_3: number = -3;
const WSLNEGATIVE_5: number = -5;
const WSLNEGATIVE_7: number = -7;
const WSLNEGATIVE_32: number = -32;
const WSLNEGATIVE_35: number = -35;
const WSLNEGATIVE_42: number = -42;
const WSLNEGATIVE_111: number = -111;
const WSLNEGATIVE_511: number = -511;
const WSLNEGATIVE_65536: number = -65536;
const WSLDECIMAL_1: number = 1.5;
const WSLDECIMAL_7: number = 7.5;
const WSL_2: number = 2;
const WSL_3: number = 3;
const WSL_4: number = 4;
const WSL_5: number = 5;
const WSL_6: number = 6;
const WSL_7: number = 7;
const WSL_8: number = 8;
const WSL_9: number = 9;
const WSL_10: number = 10;
const WSL_11: number = 11;
const WSL_12: number = 12;
const WSL_13: number = 13;
const WSL_14: number = 14;
const WSL_15: number = 15;
const WSL_16: number = 16;
const WSL_17: number = 17;
const WSL_18: number = 18;
const WSL_19: number = 19;
const WSL_20: number = 20;
const WSL_21: number = 21;
const WSL_22: number = 22;
const WSL_23: number = 23;
const WSL_24: number = 24;
const WSL_25: number = 25;
const WSL_27: number = 27;
const WSL_29: number = 29;
const WSL_30: number = 30;
const WSL_34: number = 34;
const WSL_35: number = 35;
const WSL_37: number = 37;
const WSL_39: number = 39;
const WSL_40: number = 40;
const WSL_41: number = 41;
const WSL_42: number = 42;
const WSL_43: number = 43;
const WSL_45: number = 45;
const WSL_46: number = 46;
const WSL_47: number = 47;
const WSL_48: number = 48;
const WSL_49: number = 49;
const WSL_52: number = 52;
const WSL_53: number = 53;
const WSL_54: number = 54;
const WSL_55: number = 55;
const WSL_58: number = 58;
const WSL_60: number = 60;
const WSL_62: number = 62;
const WSL_63: number = 63;
const WSL_64: number = 64;
const WSL_65: number = 65;
const WSL_68: number = 68;
const WSL_69: number = 69;
const WSL_72: number = 72;
const WSL_73: number = 73;
const WSL_76: number = 76;
const WSL_79: number = 79;
const WSL_83: number = 83;
const WSL_89: number = 89;
const WSL_91: number = 91;
const WSL_112: number = 112;
const WSL_128: number = 128;
const WSL_213: number = 213;
const WSL_224: number = 224;
const WSL_228: number = 228;
const WSL_249: number = 249;
const WSL_252: number = 252;
const WSL_254: number = 254;
const WSL_255: number = 255;
const WSL_354: number = 354;
const WSL_385: number = 385;
const WSL_498: number = 498;
const WSL_642: number = 642;
const WSL_645: number = 645;
const WSL_743: number = 743;
const WSL_754: number = 754;
const WSL_767: number = 767;
const WSL_932: number = 932;
const WSL_1234: number = 1234;
const WSL_1575: number = 1575;
const WSL_3478: number = 3478;
const WSL_4657: number = 4657;
const WSL_5565: number = 5565;
const WSL_7624: number = 7624;
const WSL_7804: number = 7804;
const WSL_16383: number = 16383;
const WSL_43696: number = 43696;
const WSL_53899: number = 53899;
const WSL_65493: number = 65493;
const WSL_65535: number = 65535;
const WSL_78453: number = 78453;
const WSL_79201: number = 79201;
const WSL_85732: number = 85732;
const WSL_130991: number = 130991;
const WSL_262140: number = 262140;
const WSL_134217727: number = 134217727;
const WSL_1409286144: number = 1409286144;
const WSL_4294901760: number = 4294901760;
const WSL_4294967264: number = 4294967264;
const WSL_4294967289: number = 4294967289;
const WSL_4294967294: number = 4294967294;
const WSL_4294967295: number = 4294967295;
const WSL_INT32MAX: number = 2147483647;

function myCall(method: (node: Base) => Base | string | void | null, thisArg: Base, node: Base): Node {
  thisArg.temporaryFn = method;
  const result = thisArg.temporaryFn(node);
  thisArg.temporaryFn = null;

  return result as Node;
}

class Base {
  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    return new Map();
  }
  temporaryFn: ((node: Base) => Base | string | void | null) | null = null;
}
let testNodeVisitCount = 0;
class Node extends Base {
  isBecome: boolean = false;
  visit(visitor: Base): Base | string | null {
    testNodeVisitCount += 1;
    let className = this.constructor.name;
    if (this.isBecome) {
      className = 'IdentityExpression';
    }
    if (['IntLiteral', 'UintLiteral', 'DoubleLiteral', 'FloatLiteral'].includes(className)) {
      className = 'GenericLiteral';
    }
    if (['IntLiteralType', 'UintLiteralType', 'DoubleLiteralType', 'FloatLiteralType'].includes(className)) {
      className = 'GenericLiteralType';
    }
    let methodName: string = `visit${className}`;
    let visitFunc: (node: Base) => Base | string | void | null = visitor.mapReturn().get(methodName) as (node: Base) => Base | string | void | null;
    if (!visitFunc) {
      throw new Error(`No visit function for ${this.constructor.name} in ${visitor.constructor.name}`);
    }
    let returnValue: Node | Base | string | null = myCall(visitFunc, visitor, this) as Node;
    return returnValue;
  }

  static visit(node: Node | null, visitor: Base): Node | null {
    if (node != null) {
      return node.visit(visitor) as Node | null;
    }
    return node;
  }

  unify(unificationContext: UnificationContext, other: Node): boolean {
    if (other == null) {
      throw new Error('Null other');
    }
    let unifyThis = this.unifyNode;
    let unifyOther: Node = other.unifyNode;
    if (unifyThis === unifyOther) {
      return true;
    }
    if (unifyOther.typeVariableUnify(unificationContext, unifyThis)) {
      return true;
    }
    return unifyThis.unifyImpl(unificationContext, unifyOther);
  }

  unifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    if (other.typeVariableUnify(unificationContext, this)) {
      return true;
    }
    return this === other;
  }

  typeVariableUnify(unificationContext: UnificationContext, other: Node): boolean {
    return false;
  }

  typeVariableUnifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    let realThis: Node = unificationContext.find(this);
    if (realThis !== this) {
      return realThis.unify(unificationContext, other);
    }
    unificationContext.union(this, other);
    return true;
  }

  // Most type variables don't care about this.
  prepareToVerify(unificationContext: UnificationContext): void {}

  commitUnification(unificationContext: UnificationContext): void {}

  get unifyNode(): Node {
    if (this.isBecome) {
      return this.target.unifyNode;
    } else {
      return this;
    }
  }
  get isUnifiable(): boolean {
    return false;
  }
  get isLiteral(): boolean {
    return false;
  }

  get isNative(): boolean {
    return false;
  }

  conversionCost(unificationContext: UnificationContext): number {
    return 0;
  }
  equals(other: Node): UnificationContext | null {
    let unificationContext = new UnificationContext(Array<Node>());
    if (this.unify(unificationContext, other) && unificationContext.verify().result) {
      return unificationContext;
    }
    return null;
  }

  equalsWithCommit(other: Node): UnificationContext | null {
    let unificationContext = this.equals(other);
    if (unificationContext == null) {
      return null;
    }
    unificationContext.commit();
    return unificationContext;
  }

  commit(): TypeRef {
    let unificationContext = new UnificationContext(Array<Node>());
    unificationContext.addExtraNode(this);
    let result = unificationContext.verify();
    if (!result.result) {
      throw new WTypeError('node.origin.originString', `Could not infer type: ${result.reason}`);
    }
    unificationContext.commit();
    return unificationContext.find(this) as TypeRef;
  }

  substitute(parameters: Array<Node>, argumentList: Array<Node>): HmType {
    return this.visit(new Substitution(parameters, argumentList)) as HmType;
  }

  substituteToUnification(parameters: Array<Node>, unificationContext: UnificationContext): HmType {
    return this.substitute(
      parameters,
      parameters.map((parameter: Node) => unificationContext.find(parameter))
    );
  }
  origin: LexerToken | null = null;
  _name: string = '';

  get name(): string {
    return this._name;
  }
  set name(newValue: string) {
    this._name = newValue;
  }

  get isPrimitive(): boolean {
    return false;
  }

  _struct1: Node | null = null;
  get struct1(): Node {
    return this._struct1!;
  }
  set struct1(newValue: Node) {
    this._struct1 = newValue;
  }

  _body: Node | null = null;
  get body(): Node {
    return this._body!;
  }
  set body(newValue: Node) {
    this._body = newValue;
  }

  _initialization: Expression | null = null;
  get initialization(): Expression | null {
    return this._initialization;
  }
  set initialization(newValue: Expression | null) {
    this._initialization = newValue;
  }

  _condition: HmValue | null = null;
  get condition(): HmValue | null {
    return this._condition;
  }
  set condition(newValue: HmValue | null) {
    this._condition = newValue;
  }

  _increment: Expression | null = null;
  get increment(): Expression | null {
    return this._increment;
  }
  set increment(newValue: Expression | null) {
    this._increment = newValue;
  }

  _target: HmValue | null = null;
  get target(): HmValue {
    return this._target!;
  }
  set target(newValue: HmValue) {
    this._target = newValue;
  }

  _hmType: HmType | null = null;
  get hmType(): HmType | null {
    return this._hmType;
  }
  set hmType(newValue: HmType | null) {
    this._hmType = newValue;
  }

  program: Program | null = null;
  returnEPtr: EPtr | null = null;
  resultEPtr: EPtr | null = null;
  _ePtr: EPtr | null = null;

  get ePtr(): EPtr | null {
    return this._ePtr;
  }
  set ePtr(newValue: EPtr | null) {
    this._ePtr = newValue;
  }

  _kind: string | null = null;

  set kind(newValue: string | null) {
    this._kind = newValue;
  }
  get kind(): string | null {
    return this._kind;
  }

  verifyAsParameter(unificationContext: UnificationContext): VerifyResult {
    return new VerifyResult(false);
  }

  verifyAsArgument(unificationContext: UnificationContext): VerifyResult {
    return new VerifyResult(false);
  }

  toString(): string {
    if (this.isBecome) {
      return `(${this.target.toString()})`;
    } else {
      return 'Node';
    }
  }

  //重写方法
  visitNativeType(node: Node): Node {
    return node;
  }

  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    return new Map<string, (node: Base) => void | Base | string | null>();
  }
}

class HmValue extends Node {
  constructor() {
    super();
  }
  _addressSpace: string | null = null;
  get addressSpace(): string | null {
    if (this.isBecome) {
      return this.target.addressSpace;
    } else {
      return this._addressSpace;
    }
  }
  set addressSpace(addressSpace: string | null) {
    this._addressSpace = addressSpace;
  }

  get kind(): string | null {
    return 'Value';
  }
  set kind(newValue: string | null) {
    this._kind = newValue;
  }

  get isConstexpr(): boolean {
    if (this.isBecome) {
      return this.target.isConstexpr;
    } else {
      return false;
    }
  }

  get isLValue(): boolean {
    if (this.isBecome) {
      return this.target.isLValue;
    } else {
      return false;
    }
  }

  get notLValueReason(): string | null {
    return null;
  }

  get notLValueReasonString(): string {
    let result = this.notLValueReason;
    if (result != null) {
      return `\n${result!}`;
    }
    return '';
  }

  become(otherValue: Expression): void {
    let origin: LexerToken | null = this.origin;
    this.isBecome = true;
    this.origin = origin;
    this.target = otherValue;
  }
}

class HmType extends Node {
  typeParameters: Array<Node> = new Array<Node>();
  _size: number | null = -1;
  canRepresent: (value: number) => boolean = (value: number) => false;
  get kind(): null | string {
    return 'Type';
  }
  set kind(newValue: null | string) {}

  get isPtr(): boolean {
    return false;
  }

  get isArray(): boolean {
    return false;
  }

  get isArrayRef(): boolean {
    return false;
  }

  get isRef(): boolean {
    return this.isPtr || this.isArrayRef;
  }

  get isNumber(): boolean {
    return false;
  }

  get isInt(): boolean {
    return false;
  }

  get isFloating(): boolean {
    return false;
  }

  get protocol1(): ProtocolRef | null {
    return null;
  }

  inherits(protocol1: ProtocolRef | null): VerifyResult {
    if (protocol1 == null) {
      return new VerifyResult(true);
    }
    return protocol1!.hasHeir(this);
  }

  get instantiatedType(): HmType {
    return this.visit(new InstantiateImmediates()) as HmType;
  }

  argumentForAndOverload(origin: LexerToken, value: HmValue): HmValue {
    return new MakePtrExpression(origin, value);
  }

  argumentTypeForAndOverload(origin: LexerToken): ReferenceType {
    return new PtrType(origin, 'thread', this);
  }

  returnTypeFromAndOverload(origin: LexerToken): void {
    throw new WTypeError(`Type error at ${origin.originString}`, ` By-pointer overload returned non-pointer type: ${this.toString()}`);
  }

  get size(): number | null {
    return this._size;
  }
  set size(value: number | null) {
    this._size = value;
  }
  get elementType(): HmType | null {
    return null;
  }

  get numElements(): Expression | null {
    return null;
  }

  get addressSpace(): string {
    return '';
  }

  typeArguments: Array<Node> = new Array<Node>();

  populateDefaultValue(buffer: EBuffer, offset: number): TypeRef | null | void {
    return null;
  }

  allValues(): Array<ResultMemberInfo> {
    let re: Array<ResultMemberInfo> = Array<ResultMemberInfo>();
    for (let i = 0; i <= WSL_0XFF; i++) {
      re.push(new ResultMemberInfo(i, `${i}`));
    }
    return re;
  }
}

class ReferenceType extends HmType {
  private _addressSpace: string;
  private _elementType: HmType | null;

  constructor(origin: LexerToken | null, addressSpace: string, elementType: HmType | null) {
    if (elementType == null) {
      throw new Error('Null elementType');
    }
    if (origin == null) {
      throw new Error('Null origin');
    }
    validateAddressSpace(addressSpace);
    super();
    this.origin = origin;
    this._addressSpace = addressSpace;
    this._elementType = elementType;
  }
  get addressSpace(): string {
    return this._addressSpace;
  }

  get size(): number {
    return 1;
  }
  set size(value: number) {}

  get elementType(): HmType | null {
    return this._elementType;
  }

  populateDefaultValue(buffer: EBuffer, offset: number | null): TypeRef | null | void {
    buffer.eBufferSet(offset, null);
  }
}

class Expression extends HmValue {
  constructor(origin: LexerToken | null) {
    super();
    this.origin = origin;
  }
  _value: boolean | number | null = null;
  get value(): boolean | number | null {
    return this._value;
  }
  set value(newValue: boolean | number | null) {
    this._value = newValue;
  }

  _lValue: HmValue | null = null;
  get lValue(): HmValue | null {
    return this._lValue;
  }
  set lValue(lValue: HmValue | null) {
    this._lValue = lValue;
  }

  get valueForSelectedType(): number {
    return 0;
  }
}

class Rewriter extends Base {
  private mappingPrivate: Map<HmValue, HmValue> = new Map<HmValue, HmValue>();
  constructor() {
    super();
    this.mappingPrivate = new Map<HmValue, HmValue>();
  }

  mapNode(oldItem: HmValue, newItem: HmValue): HmValue {
    this.mappingPrivate.set(oldItem, newItem);
    return newItem;
  }

  getMapping(oldItem: HmValue | null): HmValue | null {
    if (oldItem == null) {
      return oldItem;
    }
    let result = this.mappingPrivate.get(oldItem!);
    if (result != null) {
      return result;
    }
    return oldItem;
  }

  // We return identity for anything that is not inside a function/struct body. When processing
  // function bodies, we only recurse into them and never out of them - for example if there is a
  // function call to another function then we don't rewrite the other function. This is how we stop
  // that.
  visitFuncDef(node: Node): FuncDef {
    return node as FuncDef;
  }

  visitNativeFunc(node: Node): NativeFunc {
    return node as NativeFunc;
  }

  visitNativeFuncInstance(node: Node): NativeFuncInstance {
    return node as NativeFuncInstance;
  }

  visitNativeType(node: Node): NativeType {
    return node as NativeType;
  }

  visitTypeDef(node: Node): TypeDef {
    return node as TypeDef;
  }

  visitStructType(node: Node): StructType {
    return node as StructType;
  }

  visitConstexprTypeParameter(node: Node): ConstexprTypeParameter {
    return node as ConstexprTypeParameter;
  }

  visitProtocolDecl(node: Node): ProtocolDecl {
    return node as ProtocolDecl;
  }

  visitEnumType(node: Node): EnumType {
    return node as EnumType;
  }

  // This is almost wrong. We instantiate Func in Substitution in ProtocolDecl. Then, we end up
  // not rewriting type variables. I think that just works because not rewriting them there is OK.
  // Everywhere else, it's mandatory that we don't rewrite these because we always assume that
  // type variables are outside the scope of rewriting.
  visitTypeVariable(node: Node): TypeVariable {
    return node as TypeVariable;
  }

  visitProtocolFuncDecl(node: Node): ProtocolFuncDecl {
    const newNode = node as ProtocolFuncDecl;
    let result: ProtocolFuncDecl = new ProtocolFuncDecl(
      newNode.origin!,
      newNode.name,
      newNode.returnType.visit(this) as HmType,
      newNode.typeParameters.map((parameter: Node): Node => {
        return parameter.visit(this) as Node;
      }),
      newNode.parameters.map((parameter: FuncParameter): FuncParameter => parameter.visit(this) as FuncParameter),
      newNode.isCast,
      newNode.shaderType
    );
    result.protocolDecl = newNode.protocolDecl;
    result.possibleOverloads = newNode.possibleOverloads;
    return result;
  }

  visitNativeTypeInstance(node: Node): NativeTypeInstance {
    const newNode = node as NativeTypeInstance;
    return new NativeTypeInstance(
      newNode.hmType!.visit(this) as Node,
      newNode.typeArguments!.map((argument: Node): TypeVariable => argument.visit(this) as TypeVariable)
    );
  }

  visitFuncParameter(node: Node): FuncParameter {
    let result: FuncParameter = new FuncParameter(node.origin!, node.name, node.hmType!.visit(this) as HmType);
    this.mapNode(node as HmValue, result);
    result.ePtr = node.ePtr;
    return result;
  }

  visitVariableDecl(node: Node): VariableDecl {
    let result: VariableDecl = new VariableDecl(
      (node as VariableDecl).origin!,
      node.name,
      node.hmType!.visit(this) as Node,
      Node.visit((node as VariableDecl).initializer, this) as HmValue | null
    );
    this.mapNode(node as HmValue, result);
    result.ePtr = node.ePtr;
    return result;
  }

  visitBlock(node: Node): Block {
    let result: Block = new Block((node as Block).originStr);
    for (let statement of (node as Block).statements as Array<Node>) {
      result.add(statement.visit(this) as Node);
    }
    return result;
  }

  visitCommaExpression(node: Node): CommaExpression {
    return new CommaExpression(
      node.origin!,
      (node as CommaExpression).list.map((expression: HmValue): HmValue => {
        let result: HmValue = expression.visit(this) as HmValue;
        if (!result) {
          throw new Error(`Null result from ${expression}`);
        }
        return result;
      })
    );
  }

  visitProtocolRef(node: Node): ProtocolRef {
    return node as ProtocolRef;
  }

  visitTypeRef(node: Node): TypeRef {
    let result: TypeRef = new TypeRef(
      node.origin!,
      node.name,
      (node as TypeRef).typeArguments!.map((args: Node) => args.visit(this) as Node)
    );
    result.hmType = Node.visit(node.hmType, this) as HmType;
    return result;
  }

  visitField(node: Node): Field {
    return new Field(node.origin!, node.name, node.hmType!.visit(this) as HmType);
  }

  visitEnumMember(node: EnumMember): EnumMember {
    return new EnumMember(node.origin!, node.name, node.value!.visit(this) as Expression);
  }

  visitEnumLiteral(node: Node): EnumLiteral {
    const newNode = node as EnumLiteral;
    let result: EnumLiteral = new EnumLiteral(newNode.origin!, newNode.member);
    result.ePtr = newNode.ePtr;
    return result;
  }

  visitReferenceType(node: ReferenceType): ReferenceType {
    if (node instanceof PtrType) {
      return new PtrType(node.origin, node.addressSpace, node.elementType!.visit(this) as HmType);
    }
    return new ArrayRefType(node.origin, node.addressSpace, node.elementType!.visit(this) as HmType);
  }

  visitPtrType(node: Node): PtrType {
    return this.visitReferenceType(node as PtrType) as PtrType;
  }

  visitArrayRefType(node: Node): ArrayRefType {
    return this.visitReferenceType(node as ArrayRefType) as ArrayRefType;
  }

  visitArrayType(node: Node): ArrayType {
    return new ArrayType(node.origin!, (node as ArrayType).elementType.visit(this) as HmType, (node as ArrayType).numElements!.visit(this) as Expression);
  }

  visitAssignment(node: Node): Assignment {
    let result: Assignment = new Assignment(node.origin!, (node as Assignment).lhs.visit(this) as HmValue, (node as Assignment).rhs.visit(this) as HmValue);
    result.hmType = Node.visit(node.hmType, this) as HmType | null;
    return result;
  }

  visitReadModifyWriteExpression(node: Node): ReadModifyWriteExpression {
    const newNode = node as ReadModifyWriteExpression;
    let result: ReadModifyWriteExpression = new ReadModifyWriteExpression(newNode.origin!, newNode.lValue!.visit(this) as Expression);
    result.oldValueVar = newNode.oldValueVar.visit(this) as AnonymousVariable;
    result.newValueVar = newNode.newValueVar.visit(this) as AnonymousVariable;
    result.newValueExp = newNode.newValueExp!.visit(this) as Expression;
    result.resultExp = newNode.resultExp!.visit(this) as VariableRef;
    return result;
  }

  visitDereferenceExpression(node: Node): DereferenceExpression {
    let result: DereferenceExpression = new DereferenceExpression(node.origin!, (node as DereferenceExpression).ptr.visit(this) as HmValue);
    result.hmType = Node.visit(node.hmType, this) as HmType | null;
    result.addressSpace = (node as DereferenceExpression).addressSpace;
    return result;
  }

  handlePropertyAccessExpression(result: PropertyAccessExpression, node: PropertyAccessExpression): void {
    result.possibleGetOverloads = node.possibleGetOverloads;
    result.possibleSetOverloads = node.possibleSetOverloads;
    result.possibleAndOverloads = node.possibleAndOverloads;
    result.baseType = Node.visit(node.baseType, this) as HmType | null;
    result.callForGet = Node.visit(node.callForGet, this) as CallExpression | null;
    result.resultTypeForGet = Node.visit(node.resultTypeForGet, this) as HmType | null;
    result.callForAnd = Node.visit(node.callForAnd, this) as CallExpression | null;
    result.resultTypeForAnd = Node.visit(node.resultTypeForAnd, this) as HmType | null;
    result.callForSet = Node.visit(node.callForSet, this) as CallExpression | null;
    result.errorForSet = node.errorForSet;
    result.updateCalls();
  }

  visitDotExpression(node: Node): DotExpression {
    let result: DotExpression = new DotExpression(
      (node as DotExpression).origin!,
      ((node as DotExpression).struct1 as Expression).visit(this) as Expression,
      (node as DotExpression).fieldName
    );
    this.handlePropertyAccessExpression(result, node as PropertyAccessExpression);
    return result;
  }

  visitIndexExpression(node: Node): IndexExpression {
    const newNode = node as IndexExpression;
    let result: IndexExpression = new IndexExpression(newNode.origin!, newNode.array.visit(this) as Expression, newNode.index.visit(this) as Expression);
    this.handlePropertyAccessExpression(result, newNode);
    return result;
  }

  visitMakePtrExpression(node: Node): MakePtrExpression {
    let result: MakePtrExpression = new MakePtrExpression((node as MakePtrExpression).origin!, (node as MakePtrExpression).lValue!.visit(this) as Expression);
    result.ePtr = node.ePtr;
    return result;
  }

  visitMakeArrayRefExpression(node: Node): MakeArrayRefExpression {
    let result: MakeArrayRefExpression = new MakeArrayRefExpression(
      (node as MakeArrayRefExpression).origin!,
      (node as MakeArrayRefExpression).lValue!.visit(this) as HmValue
    );
    if ((node as MakeArrayRefExpression).numElements != null) {
      result.numElements = (node as MakeArrayRefExpression).numElements!.visit(this) as Expression;
    }
    result.ePtr = (node as MakeArrayRefExpression).ePtr;
    return result;
  }

  visitConvertPtrToArrayRefExpression(node: Node): ConvertPtrToArrayRefExpression {
    const newNode = node as ConvertPtrToArrayRefExpression;
    let result: ConvertPtrToArrayRefExpression = new ConvertPtrToArrayRefExpression(newNode.origin!, newNode.lValue!.visit(this) as VariableRef);
    result.ePtr = node.ePtr;
    return result;
  }

  visitVariableRef(node: Node): VariableRef {
    let result: VariableRef = new VariableRef((node as VariableRef).origin!, node.name);
    result.variable = this.getMapping((node as VariableRef).variable);
    return result;
  }

  visitReturn(node: Node): Return {
    return new Return(node.origin!, Node.visit((node as Return).value, this) as HmValue);
  }

  visitContinue(node: Node): Continue {
    const newNode = node as Continue;
    return new Continue(newNode.origin!);
  }

  visitBreak(node: Node): Break {
    const newNode = node as Break;
    return new Break(newNode.origin!);
  }

  visitTrapStatement(node: Node): TrapStatement {
    return new TrapStatement(node.origin!);
  }

  visitGenericLiteral(node: Node): GenericLiteral {
    // FIXME: This doesn't seem right.
    let newNode = node as GenericLiteral;
    let result: GenericLiteral = new GenericLiteral(newNode.origin!, newNode.value as number);
    result.hmType = newNode.hmType!.visit(this) as HmType;
    result.ePtr = node.ePtr;
    return result;
  }

  visitGenericLiteralType(node: Node): GenericLiteralType {
    let newNode = node as GenericLiteralType;
    let result: GenericLiteralType = new GenericLiteralType(newNode.origin!, newNode.value, newNode._config);
    result.hmType = Node.visit(newNode.hmType, this) as HmType | null;
    result.preferredType = newNode.preferredType.visit(this) as TypeRef;
    return result;
  }

  visitBoolLiteral(node: Node): BoolLiteral {
    return node as BoolLiteral;
  }

  visitNullLiteral(node: Node): NullLiteral {
    const newNode = node as NullLiteral;
    let result: NullLiteral = new NullLiteral(newNode.origin!);
    result.hmType = newNode.hmType!.visit(this) as HmType;
    result.ePtr = newNode.ePtr;
    return result;
  }

  visitNullType(node: Node): NullType {
    const newNode = node as NullType;
    let result: NullType = new NullType(newNode.origin!);
    result.hmType = Node.visit(newNode.hmType, this) as HmType | null;
    return result;
  }

  processDerivedCallData(node: CallExpression, result: CallExpression): CallExpression | null {
    let handleTypeArguments: (param: Array<Node> | null) => null | Array<Node> = (actualTypeArguments: Array<Node> | null) => {
      if (actualTypeArguments != null) {
        return actualTypeArguments.map((actualTypeArgument: Node) => actualTypeArgument.visit(this) as HmType);
      } else {
        return null;
      }
    };
    result.actualTypeArguments = handleTypeArguments(node.actualTypeArguments)!;
    result.instantiatedActualTypeArguments = handleTypeArguments(node.instantiatedActualTypeArguments)!;
    let argumentTypes: Array<HmType> = node.argumentTypes;
    if (argumentTypes.length > 0) {
      result.argumentTypes = argumentTypes.map((argumentType: HmType) => argumentType.visit(this) as HmType);
    }
    result.func1 = node.func1;
    result.nativeFuncInstance = node.nativeFuncInstance;
    result.possibleOverloads = node.possibleOverloads;
    if (node.isCast) {
      const newValue = node.returnType!.visit(this);
      result.setCastData(newValue as TypeRef);
    }
    result.resultType = Node.visit(node.resultType, this);
    result.resultEPtr = node.resultEPtr;
    return result;
  }

  visitCallExpression(node: Node): CallExpression | null {
    const newNode = node as CallExpression;
    let result: CallExpression = new CallExpression(
      newNode.origin!,
      node.name,
      newNode.typeArguments.map((typeArgument: Node) => typeArgument.visit(this) as Node),
      newNode.argumentList.map((argument: HmValue) => Node.visit(argument, this) as HmValue)
    );
    return this.processDerivedCallData(newNode, result);
  }

  visitFunctionLikeBlock(node: Node): FunctionLikeBlock {
    let result: FunctionLikeBlock = new FunctionLikeBlock(
      (node as FunctionLikeBlock).origin!,
      Node.visit((node as FunctionLikeBlock).returnType, this) as HmType,
      (node as FunctionLikeBlock).argumentList.map((argument: Node) => argument.visit(this) as Node),
      (node as FunctionLikeBlock).parameters.map((parameter: FuncParameter) => parameter.visit(this) as FuncParameter),
      (node as FunctionLikeBlock).body.visit(this) as Block
    );
    result.returnEPtr = node.returnEPtr;
    return result;
  }

  visitLogicalNot(node: Node): LogicalNot {
    let newNode = node as LogicalNot;
    let result: LogicalNot = new LogicalNot(newNode.origin!, newNode.operand.visit(this) as HmValue);
    result.ePtr = node.ePtr;
    return result;
  }

  visitLogicalExpression(node: Node): LogicalExpression {
    let newNode = node as LogicalExpression;
    let result: LogicalExpression = new LogicalExpression(
      newNode.origin!,
      newNode.text,
      newNode.left.visit(this) as HmValue,
      newNode.right.visit(this) as HmValue
    );
    result.ePtr = newNode.ePtr;
    return result;
  }

  visitIfStatement(node: Node): IfStatement {
    let newNode = node as IfStatement;
    return new IfStatement(newNode.origin!, newNode.conditional.visit(this) as HmValue, newNode.body.visit(this) as Node, Node.visit(newNode.elseBody, this));
  }

  visitWhileLoop(node: Node): WhileLoop {
    let newNode = node as WhileLoop;
    return new WhileLoop(newNode.origin!, newNode.conditional.visit(this) as HmValue, newNode.body.visit(this) as Node);
  }

  visitDoWhileLoop(node: Node): DoWhileLoop {
    let newNode = node as DoWhileLoop;
    return new DoWhileLoop(newNode.origin!, newNode.body.visit(this) as Block, newNode.conditional.visit(this) as HmValue);
  }

  visitForLoop(node: Node): ForLoop {
    let newNode = node as ForLoop;
    return new ForLoop(
      newNode.origin!,
      Node.visit(newNode.initialization, this) as Expression | null,
      Node.visit(newNode.condition, this) as HmValue | null,
      Node.visit(newNode.increment, this) as Expression | null,
      newNode.body.visit(this) as Node
    );
  }

  visitSwitchStatement(node: Node): SwitchStatement {
    let newNode = node as SwitchStatement;
    let result: SwitchStatement = new SwitchStatement(newNode.origin!, Node.visit(newNode.value, this) as HmValue);
    for (let switchCase of newNode.switchCases) {
      result.add(switchCase.visit(this) as SwitchCase);
    }
    result.hmType = Node.visit(newNode.hmType, this) as HmType;
    return result;
  }

  visitSwitchCase(node: Node): SwitchCase {
    let newNode = node as SwitchCase;
    return new SwitchCase(newNode.origin!, Node.visit(newNode.value, this) as Expression | null, newNode.body.visit(this) as Block);
  }

  visitAnonymousVariable(node: Node): AnonymousVariable {
    let newNode = node as AnonymousVariable;
    let result: AnonymousVariable = new AnonymousVariable(newNode.origin!, newNode.hmType!.visit(this) as HmType);
    result._index = newNode._index;
    this.mapNode(newNode as HmValue, result);
    result.ePtr = newNode.ePtr;
    return result;
  }

  visitIdentityExpression(node: Node): IdentityExpression {
    return new IdentityExpression(node.target.visit(this) as HmValue);
  }

  visitTypeOrVariableRef(node: Node): TypeOrVariableRef {
    return new TypeOrVariableRef(node.origin!, node.name);
  }

  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    let methods: Map<string, (node: Base) => void | Base | string | null> = new Map<string, (node: Base) => void | Base | string | null>();
    methods.set('visitNativeType', this.visitNativeType as (node: Base) => NativeType);
    methods.set('visitTypeDef', this.visitTypeDef as (node: Base) => TypeDef);
    methods.set('visitTypeRef', this.visitTypeRef as (node: Base) => TypeRef);
    methods.set('visitNativeFunc', this.visitNativeFunc as (node: Base) => NativeFunc);

    methods.set('visitFuncParameter', this.visitFuncParameter as (node: Base) => FuncParameter);
    methods.set('visitFuncDef', this.visitFuncDef as (node: Base) => FuncDef);
    methods.set('visitBlock', this.visitBlock as (node: Base) => Block);
    methods.set('visitReturn', this.visitReturn as (node: Base) => Return);

    methods.set('visitCallExpression', this.visitCallExpression as (node: Base) => CallExpression | null);
    methods.set('visitVariableRef', this.visitVariableRef as (node: Base) => VariableRef);
    methods.set('visitGenericLiteral', this.visitGenericLiteral as (node: Base) => IntLiteral);
    methods.set('visitGenericLiteralType', this.visitGenericLiteralType as (node: Base) => GenericLiteralType);

    methods.set('visitIfStatement', this.visitIfStatement as (node: Base) => IfStatement);
    methods.set('visitBoolLiteral', this.visitBoolLiteral as (node: Base) => BoolLiteral);
    methods.set('visitLogicalNot', this.visitLogicalNot as (node: Base) => LogicalNot);
    methods.set('visitProtocolDecl', this.visitProtocolDecl as (node: Base) => ProtocolDecl);

    methods.set('visitProtocolFuncDecl', this.visitProtocolFuncDecl as (node: Base) => ProtocolFuncDecl);
    methods.set('visitTypeVariable', this.visitTypeVariable as (node: Base) => TypeVariable);
    methods.set('visitCommaExpression', this.visitCommaExpression as (node: Base) => CommaExpression);
    methods.set('visitVariableDecl', this.visitVariableDecl as (node: Base) => VariableDecl);

    methods.set('visitProtocolRef', this.visitProtocolRef as (node: Base) => ProtocolRef);
    methods.set('visitStructType', this.visitStructType as (node: Base) => StructType);
    methods.set('visitField', this.visitField as (node: Base) => Field);
    methods.set('visitTypeOrVariableRef', this.visitTypeOrVariableRef as (node: Base) => TypeOrVariableRef);

    methods.set('visitAssignment', this.visitAssignment as (node: Base) => Assignment);
    methods.set('visitDotExpression', this.visitDotExpression as (node: Base) => DotExpression);
    methods.set('visitLogicalExpression', this.visitLogicalExpression as (node: Base) => LogicalExpression);
    methods.set('visitPtrType', this.visitPtrType as (node: Base) => PtrType);

    methods.set('visitMakePtrExpression', this.visitMakePtrExpression as (node: Base) => MakePtrExpression);
    methods.set('visitDereferenceExpression', this.visitDereferenceExpression as (node: Base) => DereferenceExpression);
    methods.set('visitTrapStatement', this.visitTrapStatement as (node: Base) => TrapStatement);
    methods.set('visitArrayRefType', this.visitArrayRefType as (node: Base) => ArrayRefType);

    methods.set('visitConstexprTypeParameter', this.visitConstexprTypeParameter as (node: Base) => ConstexprTypeParameter);
    methods.set('visitArrayType', this.visitArrayType as (node: Base) => ArrayType);
    methods.set('visitIdentityExpression', this.visitIdentityExpression as (node: Base) => IdentityExpression);

    methods.set('visitAnonymousVariable', this.visitAnonymousVariable as (node: Base) => AnonymousVariable);
    methods.set('visitEnumType', this.visitEnumType as (node: Base) => EnumType);
    methods.set('visitNullLiteral', this.visitNullLiteral as (node: Base) => BoolLiteral);

    methods.set('visitNullType', this.visitNullType as (node: Base) => NullType);
    methods.set('visitMakeArrayRefExpression', this.visitMakeArrayRefExpression as (node: Base) => MakeArrayRefExpression);
    methods.set('visitIndexExpression', this.visitIndexExpression as (node: Base) => IndexExpression);
    methods.set('visitSwitchStatement', this.visitSwitchStatement as (node: Base) => SwitchStatement);

    methods.set('visitSwitchCase', this.visitSwitchCase as (node: Base) => SwitchCase);
    methods.set('visitBreak', this.visitBreak as (node: Base) => Break);
    methods.set('visitEnumLiteral', this.visitEnumLiteral as (node: Base) => EnumLiteral);
    methods.set('visitForLoop', this.visitForLoop as (node: Base) => ForLoop);

    methods.set('visitConvertPtrToArrayRefExpression', this.visitConvertPtrToArrayRefExpression as (node: Base) => ConvertPtrToArrayRefExpression);
    methods.set('visitDoWhileLoop', this.visitDoWhileLoop as (node: Base) => DoWhileLoop);
    methods.set('visitWhileLoop', this.visitWhileLoop as (node: Base) => WhileLoop);
    methods.set('visitContinue', this.visitContinue as (node: Base) => Continue);

    return methods;
  }
}

class Visitor extends Base {
  visitProgram(node: Node): void {
    for (let statement of (node as Program).topLevelStatements) {
      statement.visit(this);
    }
  }

  visitFunc(node: Node): void {
    let newNode = node as Func;
    newNode.returnType.visit(this);
    for (let typeParameter of newNode.typeParameters) {
      typeParameter.visit(this);
    }
    for (let parameter of newNode.parameters) {
      parameter.visit(this);
    }
  }

  visitProtocolFuncDecl(node: Node): void {
    this.visitFunc(node);
  }

  visitFuncParameter(node: Node): void {
    node.hmType!.visit(this);
  }

  visitFuncDef(node: Node): void {
    this.visitFunc(node);
    node.body.visit(this);
  }

  visitNativeFunc(node: Node): void {
    this.visitFunc(node);
  }

  visitNativeFuncInstance(node: Node): void {
    let newNode = node as NativeFuncInstance;
    this.visitFunc(newNode);
    newNode.func1.visitImplementationData(newNode.implementationData, this);
  }

  visitBlock(node: Node): void {
    for (let statement of (node as Block).statements) {
      statement.visit(this);
    }
  }

  visitCommaExpression(node: Node): void {
    for (let expression of (node as CommaExpression).list) {
      expression.visit(this);
    }
  }

  visitProtocolRef(node: Node): void {}

  visitProtocolDecl(node: Node): void {
    let newNode = node as ProtocolDecl;
    for (let protocol1 of newNode.extends) {
      protocol1.visit(this);
    }
    for (let signature of newNode.signatures) {
      signature.visit(this);
    }
  }

  visitTypeRef(node: Node): void {
    for (let typeArgument of (node as TypeRef).typeArguments) {
      typeArgument.visit(this);
    }
  }

  visitNativeType(node: Node): void {
    for (let typeParameter of (node as NativeType).typeParameters) {
      typeParameter.visit(this);
    }
  }

  visitNativeTypeInstance(node: Node): void {
    let newNode = node as NativeTypeInstance;
    newNode.hmType!.visit(this);
    for (let typeArgument of newNode.typeArguments) {
      typeArgument.visit(this);
    }
  }

  visitTypeDef(node: Node): void {
    for (let typeParameter of (node as TypeDef).typeParameters) {
      typeParameter.visit(this);
    }
    node.hmType!.visit(this);
  }

  visitStructType(node: Node): void {
    let newNode = node as StructType;
    for (let typeParameter of newNode.typeParameters) {
      typeParameter.visit(this);
    }
    for (let field of newNode.fields) {
      field.visit(this);
    }
  }

  visitTypeVariable(node: Node): void {
    Node.visit((node as TypeVariable).protocol1, this);
  }

  visitConstexprTypeParameter(node: Node): void {
    (node as ConstexprTypeParameter).hmType!.visit(this);
  }

  visitField(node: Node): void {
    node.hmType!.visit(this);
  }

  visitEnumType(node: Node): void {
    let newNode = node as EnumType;
    newNode.baseType.visit(this);
    for (let member of newNode.members) {
      member.visit(this);
    }
  }

  visitEnumMember(node: Node): void {
    Node.visit((node as EnumMember).value, this);
  }

  visitEnumLiteral(node: Node): void {}

  visitElementalType(node: Node): void {
    (node as HmType).elementType!.visit(this);
  }

  visitReferenceType(node: Node): void {
    this.visitElementalType(node);
  }

  visitPtrType(node: Node): void {
    this.visitReferenceType(node);
  }

  visitArrayRefType(node: Node): void {
    this.visitReferenceType(node);
  }

  visitArrayType(node: Node): void {
    let newNode = node as ArrayType;
    this.visitElementalType(newNode);
    newNode.numElements!.visit(this);
  }

  visitVariableDecl(node: Node): void {
    node.hmType!.visit(this);
    Node.visit((node as VariableDecl).initializer, this);
  }

  visitAssignment(node: Node): void {
    let newNode = node as Assignment;
    newNode.lhs.visit(this);
    newNode.rhs.visit(this);
    Node.visit(newNode.hmType, this);
  }

  visitReadModifyWriteExpression(node: Node): void {
    let newNode = node as ReadModifyWriteExpression;
    newNode.lValue!.visit(this);
    newNode.oldValueVar.visit(this);
    newNode.newValueVar.visit(this);
    newNode.newValueExp!.visit(this);
    newNode.resultExp!.visit(this);
  }

  visitDereferenceExpression(node: Node): void {
    (node as DereferenceExpression).ptr.visit(this);
  }

  handlePropertyAccessExpression(node: Node): void {
    let newNode = node as PropertyAccessExpression;
    Node.visit(newNode.baseType, this);
    Node.visit(newNode.callForGet, this);
    Node.visit(newNode.resultTypeForGet, this);
    Node.visit(newNode.callForAnd, this);
    Node.visit(newNode.resultTypeForAnd, this);
    Node.visit(newNode.callForSet, this);
  }

  visitDotExpression(node: Node): void {
    node.struct1!.visit(this);
    this.handlePropertyAccessExpression(node);
  }

  visitIndexExpression(node: Node): void {
    let newNode = node as IndexExpression;
    newNode.array.visit(this);
    newNode.index.visit(this);
    this.handlePropertyAccessExpression(newNode);
  }

  visitMakePtrExpression(node: Node): void {
    (node as MakePtrExpression).lValue!.visit(this);
  }

  visitMakeArrayRefExpression(node: Node): void {
    let newNode = node as MakeArrayRefExpression;
    newNode.lValue!.visit(this);
    Node.visit(newNode.numElements, this);
  }

  visitConvertPtrToArrayRefExpression(node: Node): void {
    (node as ConvertPtrToArrayRefExpression).lValue!.visit(this);
  }

  visitVariableRef(node: Node): void {}

  visitIfStatement(node: Node): void {
    let newNode = node as IfStatement;
    newNode.conditional.visit(this);
    newNode.body.visit(this);
    Node.visit(newNode.elseBody, this);
  }

  visitWhileLoop(node: Node): void {
    let newNode = node as WhileLoop;
    newNode.conditional.visit(this);
    newNode.body.visit(this);
  }

  visitDoWhileLoop(node: Node): void {
    let newNode = node as DoWhileLoop;
    newNode.body.visit(this);
    newNode.conditional.visit(this);
  }

  visitForLoop(node: Node): void {
    let newNode = node as ForLoop;
    Node.visit(newNode.initialization, this);
    Node.visit(newNode.condition, this);
    Node.visit(newNode.increment, this);
    newNode.body.visit(this);
  }

  visitSwitchStatement(node: Node): void {
    let newNode = node as SwitchStatement;
    newNode.value.visit(this);
    for (let switchCase of newNode.switchCases) {
      switchCase.visit(this);
    }
  }

  visitSwitchCase(node: Node): void {
    let newNode = node as SwitchCase;
    Node.visit(newNode.value, this);
    newNode.body.visit(this);
  }

  visitReturn(node: Node): void {
    Node.visit((node as Return).value, this);
  }

  visitContinue(node: Node): void {}

  visitBreak(node: Node): void {}

  visitTrapStatement(node: Node): void {}

  visitGenericLiteral(node: Node): void {
    node.hmType!.visit(this);
  }

  visitGenericLiteralType(node: Node): void {
    Node.visit(node.hmType, this);
    (node as GenericLiteralType).preferredType.visit(this);
  }

  visitNullLiteral(node: Node): void {
    node.hmType!.visit(this);
  }

  visitBoolLiteral(node: Node): void {}

  visitNullType(node: Node): void {
    Node.visit(node.hmType, this);
  }

  visitCallExpression(node: Node): void {
    let newNode = node as CallExpression;
    for (let typeArgument of newNode.typeArguments) {
      typeArgument.visit(this);
    }
    for (let argument of newNode.argumentList) {
      Node.visit(argument, this);
    }
    let handleTypeArguments: (actualTypeArguments: Node[]) => void = (actualTypeArguments: Node[]) => {
      if (actualTypeArguments.length !== 0) {
        for (let argument of actualTypeArguments) {
          argument!.visit(this);
        }
      }
    };
    handleTypeArguments(newNode.actualTypeArguments!);
    handleTypeArguments(newNode.instantiatedActualTypeArguments!);
    Node.visit(newNode.nativeFuncInstance, this);
    Node.visit(newNode.returnType, this);
    Node.visit(newNode.resultType, this);
  }

  visitLogicalNot(node: Node): void {
    (node as LogicalNot).operand.visit(this);
  }

  visitLogicalExpression(node: Node): void {
    let newNode = node as LogicalExpression;
    newNode.left.visit(this);
    newNode.right.visit(this);
  }

  visitFunctionLikeBlock(node: Node): void {
    let newNode = node as FunctionLikeBlock;
    Node.visit(newNode.returnType, this);
    for (let argument of newNode.argumentList) {
      argument.visit(this);
    }
    for (let parameter of newNode.parameters) {
      parameter.visit(this);
    }
    newNode.body.visit(this);
    Node.visit(newNode.resultType, this);
  }

  visitAnonymousVariable(node: Node): void {
    Node.visit((node as AnonymousVariable).hmType, this);
  }

  visitIdentityExpression(node: Node): void {
    (node as Expression).target.visit(this);
  }
  visitTypeOrVariableRef(node: Node): void {}

  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    let methods: Map<string, (node: Base) => void | Base | string | null> = new Map<string, (node: Base) => void | Base | string | null>();
    methods.set('visitProgram', this.visitProgram as (node: Base) => void);
    methods.set('visitProtocolFuncDecl', this.visitProtocolFuncDecl as (node: Base) => void);
    methods.set('visitFuncParameter', this.visitFuncParameter as (node: Base) => void);

    methods.set('visitFuncDef', this.visitFuncDef as (node: Base) => void);
    methods.set('visitNativeFunc', this.visitNativeFunc as (node: Base) => void);
    methods.set('visitNativeFuncInstance', this.visitNativeFuncInstance as (node: Base) => void);
    methods.set('visitBlock', this.visitBlock as (node: Base) => void);

    methods.set('visitCommaExpression', this.visitCommaExpression as (node: Base) => void);
    methods.set('visitProtocolRef', this.visitProtocolRef as (node: Base) => void);
    methods.set('visitProtocolDecl', this.visitProtocolDecl as (node: Base) => void);
    methods.set('visitTypeRef', this.visitTypeRef as (node: Base) => void);

    methods.set('visitNativeType', this.visitNativeType as (node: Base) => void);
    methods.set('visitTypeDef', this.visitTypeDef as (node: Base) => void);
    methods.set('visitStructType', this.visitStructType as (node: Base) => void);

    methods.set('visitTypeVariable', this.visitTypeVariable as (node: Base) => void);
    methods.set('visitConstexprTypeParameter', this.visitConstexprTypeParameter as (node: Base) => void);
    methods.set('visitField', this.visitField as (node: Base) => void);
    methods.set('visitEnumType', this.visitEnumType as (node: Base) => void);

    methods.set('visitEnumMember', this.visitEnumMember as (node: Base) => void);
    methods.set('visitEnumLiteral', this.visitEnumLiteral as (node: Base) => void);

    methods.set('visitPtrType', this.visitPtrType as (node: Base) => void);
    methods.set('visitArrayRefType', this.visitArrayRefType as (node: Base) => void);
    methods.set('visitArrayType', this.visitArrayType as (node: Base) => void);
    methods.set('visitVariableDecl', this.visitVariableDecl as (node: Base) => void);

    methods.set('visitAssignment', this.visitAssignment as (node: Base) => void);
    methods.set('visitReadModifyWriteExpression', this.visitReadModifyWriteExpression as (node: Base) => void);
    methods.set('visitDereferenceExpression', this.visitDereferenceExpression as (node: Base) => void);
    methods.set('visitDotExpression', this.visitDotExpression as (node: Base) => void);

    methods.set('visitIndexExpression', this.visitIndexExpression as (node: Base) => void);
    methods.set('visitMakePtrExpression', this.visitMakePtrExpression as (node: Base) => void);
    methods.set('visitMakeArrayRefExpression', this.visitMakeArrayRefExpression as (node: Base) => void);
    methods.set('visitConvertPtrToArrayRefExpression', this.visitConvertPtrToArrayRefExpression as (node: Base) => void);

    methods.set('visitVariableRef', this.visitVariableRef as (node: Base) => void);
    methods.set('visitIfStatement', this.visitIfStatement as (node: Base) => void);
    methods.set('visitWhileLoop', this.visitWhileLoop as (node: Base) => void);
    methods.set('visitDoWhileLoop', this.visitDoWhileLoop as (node: Base) => void);

    methods.set('visitForLoop', this.visitForLoop as (node: Base) => void);
    methods.set('visitSwitchStatement', this.visitSwitchStatement as (node: Base) => void);
    methods.set('visitSwitchCase', this.visitSwitchCase as (node: Base) => void);
    methods.set('visitReturn', this.visitReturn as (node: Base) => void);

    methods.set('visitContinue', this.visitContinue as (node: Base) => void);
    methods.set('visitBreak', this.visitBreak as (node: Base) => void);
    methods.set('visitTrapStatement', this.visitTrapStatement as (node: Base) => void);
    methods.set('visitGenericLiteral', this.visitGenericLiteral as (node: Base) => void);

    methods.set('visitGenericLiteralType', this.visitGenericLiteralType as (node: Base) => void);
    methods.set('visitNullLiteral', this.visitNullLiteral as (node: Base) => void);
    methods.set('visitBoolLiteral', this.visitBoolLiteral as (node: Base) => void);
    methods.set('visitNullType', this.visitNullType as (node: Base) => void);

    methods.set('visitCallExpression', this.visitCallExpression as (node: Base) => void);
    methods.set('visitLogicalNot', this.visitLogicalNot as (node: Base) => void);
    methods.set('visitLogicalExpression', this.visitLogicalExpression as (node: Base) => void);
    methods.set('visitFunctionLikeBlock', this.visitFunctionLikeBlock as (node: Base) => void);

    methods.set('visitAnonymousVariable', this.visitAnonymousVariable as (node: Base) => void);
    methods.set('visitIdentityExpression', this.visitIdentityExpression as (node: Base) => void);
    methods.set('visitTypeOrVariableRef', this.visitTypeOrVariableRef as (node: Base) => void);
    methods.set('visitIndexExpression', this.visitIndexExpression as (node: Base) => void);

    return methods;
  }
}

class PropertyAccessExpression extends Expression {
  base: Expression = new Expression(null);
  private _isLValue: boolean | null = null;
  private _notLValueReason: string | null = null;
  callForGet: CallExpression | null = null;
  callForAnd: CallExpression | null = null;
  callForSet: CallExpression | null = null;
  resultTypeForGet: HmType | null = null;
  resultTypeForAnd: HmType | null = null;
  errorForSet: WTypeError | null = null;
  baseType: HmType | null = null;
  possibleGetOverloads: Array<Func> = Array();
  possibleSetOverloads: Array<Func> = Array();
  possibleAndOverloads: Array<Func> = Array();

  constructor(origin: LexerToken, base: Expression) {
    super(origin);
    this.base = base;
    this._isLValue = null;
    this.addressSpace = null;
    this._notLValueReason = null;
  }

  get resultType(): HmType {
    return this.resultTypeForGet != null ? this.resultTypeForGet! : this.resultTypeForAnd!;
  }

  set isLValue(newValue: boolean) {
    if (!this.isBecome) {
      this._isLValue = newValue;
    }
  }

  get isLValue(): boolean {
    if (this.isBecome) {
      return this.target.isLValue;
    } else {
      return this._isLValue!;
    }
  }
  set notLValueReason(newValue: string) {
    if (!this.isBecome) {
      this._notLValueReason = newValue;
    }
  }
  get notLValueReason(): string {
    if (this.isBecome) {
      return super.notLValueReason!;
    } else {
      return this._notLValueReason!;
    }
  }

  get getFuncName(): string {
    return '';
  }

  get andFuncName(): string {
    return '';
  }

  get setFuncName(): string {
    return '';
  }
  rewriteAfterCloning(): Expression {
    // At this point, node.base.isLValue will only return true if it's possible to get a pointer to it,
    // since we will no longer see any DotExpressions or IndexExpressions. Also, node is a newly created
    // node and everything beneath it is newly created, so we can edit it in-place.
    if ((this.base.isLValue || this.baseType!.isRef) && this.callForAnd != null) {
      return new DereferenceExpression(this.origin!, this.callForAnd, this.resultType, (this.callForAnd.resultType as PtrType).addressSpace);
    }

    if (this.callForGet != null) {
      return this.callForGet!;
    }

    if (this.callForAnd == null) {
      throw new Error(`${this.origin!.originString}:Property access without getter and ander:${this.toString()}`);
    }
    let anonVar = new AnonymousVariable(this.origin!, this.baseType);
    this.callForAnd!.argumentList[0] = (this.baseType!.unifyNode as HmType).argumentForAndOverload(this.origin!, VariableRef.wrap(anonVar));
    return new CommaExpression(this.origin!,
      [anonVar, new Assignment(this.origin!, VariableRef.wrap(anonVar), this.base, this.baseType),
        new DereferenceExpression(this.origin!, this.callForAnd!, this.resultType, (this.callForAnd!.resultType as PtrType).addressSpace)
      ]);
  }

  updateCalls(): void {
    if (this.callForGet != null) {
      this.callForGet!.argumentList[0] = this.base;
    }
    if (this.callForAnd != null) {
      this.callForAnd!.argumentList[0] = this.baseType!.argumentForAndOverload(this.origin!, this.base);
    }
    if (this.callForSet != null) {
      this.callForSet!.argumentList[0] = this.base;
    }
  }
  emitGet(base: VariableRef): CallExpression {
    let result = this.visit(new Rewriter()) as PropertyAccessExpression;
    result.base = base;
    result.updateCalls();
    return result.rewriteAfterCloning() as CallExpression;
  }

  emitSet(base: VariableRef, value: VariableRef): CallExpression {
    let result = this.visit(new Rewriter()) as PropertyAccessExpression;
    if (result.callForSet == null) {
      throw new WTypeError(this.origin!.originString, `Cannot emit set because: ${this.errorForSet!.typeErrorMessage}`);
    }
    result.base = base;
    result.updateCalls();
    if (result.callForSet.argumentList[result.callForSet.argumentList.length - 1] == null) {
      result.callForSet.argumentList[result.callForSet.argumentList.length - 1] = value;
    } else {
      result.callForSet.argumentList.push(value);
    }
    return result.callForSet;
  }
}

class HmProtocol extends Node {
  constructor(origin: LexerToken, name: string) {
    super();
    this.origin = origin;
    this.name = name;
  }
  get kind(): string | null {
    return 'Protocol';
  }
  set kind(newValue: string | null) {}

  toString(): string {
    return this.name;
  }
}
class ProtocolDecl extends HmProtocol {
  extends: Array<ProtocolRef> = Array();
  private _signatures: Array<ProtocolFuncDecl> = Array();
  private _signatureMap: Map<string, Array<ProtocolFuncDecl>> = new Map<string, Array<ProtocolFuncDecl>>();
  private _typeVariable: TypeVariable;

  constructor(origin: LexerToken, name: string) {
    super(origin, name);
    this._typeVariable = new TypeVariable(origin, name, null);
  }

  addExtends(protocol1: ProtocolRef): void {
    this.extends.push(protocol1);
  }

  add(signature: ProtocolFuncDecl): void {
    if (!(signature instanceof ProtocolFuncDecl)) {
      throw new Error(`Signature isn't a ProtocolFuncDecl but a ${(signature as Base).constructor.name}`);
    }
    signature.protocolDecl = this;
    this._signatures.push(signature);
    let overloads = this._signatureMap.get(signature.name);
    if (overloads == null) {
      overloads = [];
      this._signatureMap.set(signature.name, overloads);
    }
    overloads!.push(signature);
    this._signatureMap.set(signature.name, overloads!);
  }

  get signatures(): Array<ProtocolFuncDecl> {
    return this._signatures;
  }

  signaturesByName(name: string): Array<ProtocolFuncDecl> | null {
    return this._signatureMap.get(name) as Array<ProtocolFuncDecl> | null;
  }

  get typeVariable(): TypeVariable {
    return this._typeVariable;
  }

  signaturesByNameWithTypeVariable(name: string, typeVariable: TypeVariable): Array<ProtocolFuncDecl> | null {
    let substitution: Substitution = new Substitution([this.typeVariable], [typeVariable]);
    let result = this.signaturesByName(name);
    if (result == null) {
      return null;
    }
    return result.map((decl: ProtocolFuncDecl) => decl.visit(substitution) as ProtocolFuncDecl);
  }

  inherits(otherProtocol: ProtocolRef): VerifyResult {
    if (otherProtocol == null) {
      return new VerifyResult(true);
    }
    let protocolDecl: ProtocolDecl = otherProtocol.protocolDecl!;

    for (let otherSignature of protocolDecl!.signatures) {
      let signatures = this.signaturesByName(otherSignature.name);
      if (signatures == null) {
        return new VerifyResult(
          false,
          `Protocol ${this.name} does not have a function named ${otherSignature.name} (looking at signature ${otherSignature.toString()})`
        );
      }
      let overload: ResolveOverloadResult = resolveOverloadImpl(
        signatures,
        [],
        otherSignature.parameterTypes,
        otherSignature.returnTypeForOverloadResolution as TypeRef
      );
      if (overload.func1 == null) {
        const tmp =
          overload.failures!.length !== 0 ? ` (tried: ${overload.failures!.map((failure: OverloadResolutionFailure) => failure.toString()).join('; ')})` : '';
        return new VerifyResult(false, `Did not find matching signature for ${otherSignature} in ${this.name}${tmp}`);
      }
      let substitutedReturnType: HmType = overload.func1.returnType.substituteToUnification(overload.func1.typeParameters, overload.unificationContext!);
      if (substitutedReturnType.equals(otherSignature.returnType) == null) {
        return new VerifyResult(false, `Return type mismatch between ${otherSignature.returnType} and ${substitutedReturnType}`);
      }
    }
    return new VerifyResult(true);
  }

  hasHeir(type: HmType): VerifyResult {
    let substitution: Substitution = new Substitution([this._typeVariable], [type]);
    let signatures = this.signatures;
    for (let originalSignature of signatures) {
      let signature = originalSignature.visit(substitution) as ProtocolFuncDecl;
      let overload: ResolveOverloadResult = resolveOverloadImpl(
        signature.possibleOverloads,
        signature.typeParameters,
        signature.parameterTypes,
        signature.returnTypeForOverloadResolution as TypeRef
      );
      if (overload.func1 == null) {
        const tmp =
          overload.failures!.length !== 0 ?
            ' (tried: ' + overload.failures!.map((failure: OverloadResolutionFailure) => failure.toString()).join('; ') + ')' :
            '';
        return new VerifyResult(
          false,
          `Did not find matching signature for ${originalSignature.toString()} (at ${
            originalSignature.origin!.originString
          }) with type ${type.toString()}${tmp}`
        );
      }
      let substitutedReturnType: HmType = overload.func1.returnType.substituteToUnification(overload.func1!.typeParameters, overload.unificationContext!);
      if (substitutedReturnType.equals(signature.returnType) == null) {
        return new VerifyResult(
          false,
          `At signature ${originalSignature} (at ${
            originalSignature.origin!.originString
          }): return type mismatch between ${signature.returnType.toString()} and ${substitutedReturnType.toString()} in found function ${overload.func1.toDeclString()}`
        );
      }
    }
    return new VerifyResult(true);
  }

  toString(): string {
    return `protocol ${this.name} { ${this.signatures.map((decl: ProtocolFuncDecl) => decl.toString()).join('; ')}; }`;
  }
}

class AnonymousVariable extends HmValue {
  _index: number | null = null;
  index: number = 0;

  // You have to initialize the variable's value before use, but that could be quite late.
  constructor(origin: LexerToken, type: HmType | null = null) {
    super();
    this.origin = origin;
    this.index = anonymousVariableCount;
    anonymousVariableCount += 1;
    this.hmType = type;
  }

  get name(): string {
    return `anonVar<${this.index}>`;
  }
  set name(newValue: string) {}

  toString(): string {
    return this.name;
  }
}

// Note that we say that T[] is "the same type" as T[c] for any T, c. This greatly simplifies the
// language.
class ArrayRefType extends ReferenceType {
  unifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    if (!(other as HmType).isArrayRef) {
      return false;
    }

    if (this.addressSpace !== (other as ArrayRefType).addressSpace) {
      return false;
    }

    return this.elementType!.unify(unificationContext, (other as ArrayRefType).elementType!);
  }

  get isArrayRef(): boolean {
    return true;
  }

  argumentForAndOverload(origin: LexerToken, value: HmValue): HmValue {
    return value;
  }

  argumentTypeForAndOverload(origin: LexerToken): ArrayRefType {
    return this;
  }

  toString(): string {
    return `${this.elementType?.toString() ?? ''}[] ${this.addressSpace}`;
  }
}

class ArrayType extends HmType {
  private _elementType: HmType;
  private _numElements: Expression;

  constructor(origin: LexerToken, elementType: HmType, numElements: Expression) {
    if (numElements == null) {
      throw new Error('null numElements');
    }
    super();
    this.origin = origin;
    this._elementType = elementType;
    this._numElements = numElements;
  }
  get elementType(): HmType {
    return this._elementType;
  }

  get numElements(): Expression | null {
    return this._numElements;
  }

  get isPrimitive(): boolean {
    return this.elementType.isPrimitive;
  }
  set isPrimitive(newValue: boolean) {}

  get isArray(): boolean {
    return true;
  }

  get numElementsValue(): number | null {
    if (this.numElements?.isLiteral === false) {
      throw new WTypeError(`numElements is not a literal: ${this.numElements?.toString() ?? ''}`);
    }
    let value = this.numElements?.value as number | null;
    return value;
  }

  set numElementsValue(newValue: number | null) {}

  toString(): string {
    return `${this.elementType}[${this.numElements?.toString() ?? ''}]`;
  }

  get size(): number {
    return (this.elementType.size ?? 0) * (this.numElementsValue ?? 0);
  }
  set size(newVaue: number) {}

  populateDefaultValue(buffer: EBuffer, offset: number): void {
    for (let i = 0; i < (this.numElementsValue ?? 0); i++) {
      this.elementType.populateDefaultValue(buffer, offset + i * (this.elementType.size ?? 0));
    }
  }

  unifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    if (!(other instanceof ArrayType)) {
      return false;
    }

    if (!(this.numElements?.unify(unificationContext, (other as HmType).numElements!) ?? false)) {
      return false;
    }

    return this.elementType.unify(unificationContext, (other as HmType).elementType ?? new Node());
  }

  argumentForAndOverload(origin: LexerToken, value: HmValue): HmValue {
    let result: MakeArrayRefExpression = new MakeArrayRefExpression(origin, value);
    result.numElements = this.numElements!;
    return result;
  }

  argumentTypeForAndOverload(origin: LexerToken): ReferenceType {
    return new ArrayRefType(origin, 'thread', this.elementType as TypeRef | null);
  }
}

let assignmentEptr: EPtr | null;

class Assignment extends Expression {
  private _lhs: HmValue;
  private _rhs: HmValue;

  constructor(origin: LexerToken, lhs: HmValue, rhs: HmValue, type: HmType | null = null) {
    super(origin);
    this._lhs = lhs;
    this._rhs = rhs;
    this.hmType = type;
  }

  get lhs(): HmValue {
    return this._lhs;
  }

  get rhs(): HmValue {
    return this._rhs;
  }

  toString(): string {
    if (this.isBecome) {
      return `(${this.target.toString()})`;
    } else {
      return `${this.lhs.toString()} = ${this.rhs.toString()}`;
    }
  }
}

class AutoWrapper extends Rewriter {
  visitVariableRef(node: Node): VariableRef {
    return node as VariableRef;
  }

  visitTypeRef(node: Node): TypeRef {
    return node as TypeRef;
  }

  visitConstexprTypeParameter2(node: Node): VariableRef {
    return VariableRef.wrap(node as HmValue);
  }

  visitFuncParameter2(node: Node): VariableRef {
    return VariableRef.wrap(node as HmValue);
  }

  visitVariableDecl2(node: Node): VariableRef {
    return VariableRef.wrap(node as HmValue);
  }

  visitStructType2(node: Node): TypeRef {
    return TypeRef.wrap(node as HmType);
  }

  visitNativeType2(node: Node): TypeRef {
    return TypeRef.wrap(node as HmType);
  }

  visitTypeVariable2(node: Node): TypeRef {
    return TypeRef.wrap(node as HmType);
  }

  visitGenericLiteralType(node: Node): GenericLiteralType {
    return node as GenericLiteralType;
  }

  visitNullType(node: Node): NullType {
    return node as NullType;
  }

  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    let methods: Map<string, (node: Base) => void | Base | string | null> = super.mapReturn();
    methods.set('visitConstexprTypeParameter', this.visitConstexprTypeParameter2 as (node: Base) => VariableRef);
    methods.set('visitFuncParameter', this.visitFuncParameter2 as (node: Base) => VariableRef);
    methods.set('visitVariableDecl', this.visitVariableDecl2 as (node: Base) => VariableRef);
    methods.set('visitStructType', this.visitStructType2 as (node: Base) => TypeRef);
    methods.set('visitNativeType', this.visitNativeType2 as (node: Base) => TypeRef);
    methods.set('visitTypeVariable', this.visitTypeVariable2 as (node: Base) => TypeRef);
    methods.set('visitNullType', this.visitNullType as (node: Base) => NullType);
    return methods;
  }
}
// Block class
class Block extends Node {
  private _originStr: string;
  private _statements: Array<Node>;

  constructor(originStr: string) {
    super();
    this._originStr = originStr;
    this._statements = [];
  }

  get originStr(): string {
    return this._originStr;
  }

  add(statement: Node): void {
    this._statements.push(statement);
  }

  get statements(): Node[] {
    return this._statements;
  }

  toString(): string {
    if (this.statements.length === 0) {
      return '{ }';
    }
    return `{ ${this.statements.map((statement: Node) => statement.toString()).join('; ')}; }`;
  }
}

class BoolLiteral extends Expression {
  constructor(origin: LexerToken, value: boolean) {
    super(origin);
    this._value = value;
  }

  get value(): boolean | null {
    return this._value as boolean | null;
  }
  set value(newValue: boolean | null) {}

  get isConstexpr(): boolean {
    return true;
  }
  set isConstexpr(newValue: boolean) {}

  toString(): string {
    return `${this._value}`;
  }
}

// Break class
class Break extends Node {
  constructor(origin: LexerToken) {
    super();
    this.origin = origin;
  }

  toString(): string {
    return 'break';
  }
}

class CallExpression extends Expression {
  func1: Node | null;
  resultType: Node | null = null;
  argumentTypes: Array<HmType> = [];
  actualTypeArguments: Array<Node> = [];
  instantiatedActualTypeArguments: Array<Node> = [];
  possibleOverloads: Array<Func> | null = null;
  nativeFuncInstance: Func | null = null;
  private _typeArguments: Array<Node> = [];
  private _argumentList: Array<HmValue> = [];
  private _isCast: boolean = false;
  private _returnType: TypeRef | null;

  constructor(origin: LexerToken, name: string, typeArguments: Node[], argumentList: HmValue[]) {
    super(origin);
    this.name = name;
    this._typeArguments = typeArguments;
    this._argumentList = argumentList;
    this.func1 = null;
    this._isCast = false;
    this._returnType = null;
  }

  get typeArguments(): Node[] {
    return this._typeArguments;
  }
  set typeArguments(newValue: Node[]) {
    this._typeArguments = newValue;
  }
  get argumentList(): HmValue[] {
    return this._argumentList;
  }
  set argumentList(newValue: HmValue[]) {
    this._argumentList = newValue;
  }
  get isCast(): boolean {
    return this._isCast;
  }
  get returnType(): TypeRef | null {
    return this._returnType;
  }

  static resolve(
    origin: LexerToken,
    possibleOverloads: Func[] | null,
    typeParametersInScope: Node[],
    name: string,
    typeArguments: HmType[],
    argumentList: Expression[],
    argumentTypes: HmType[],
    returnType: TypeRef | null
  ): CallResolveResult {
    let call = new CallExpression(origin, name, typeArguments, argumentList);
    call.argumentTypes = argumentTypes.map((argument): HmType => argument.visit(new AutoWrapper()) as HmType);
    call.possibleOverloads = possibleOverloads;
    if (returnType !== null) {
      call.setCastData(returnType);
    }
    return new CallResolveResult(call, call.resolve(possibleOverloads, typeParametersInScope, typeArguments as Node[]));
  }

  resolve(possibleOverloads: Func[] | null, typeParametersInScope: Node[], typeArguments: Node[]): HmType {
    if (possibleOverloads == null || possibleOverloads!.length === 0) {
      throw new WTypeError(`${this.origin!.originString} Did not find any functions named ${this.name}`);
    }

    let overload: ResolveOverloadResult | null = null;
    let failures: OverloadResolutionFailure[] = [];
    for (let typeParameter of typeParametersInScope) {
      if (!(typeParameter instanceof TypeVariable)) {
        continue;
      }
      if ((typeParameter as TypeVariable).protocol1 === null) {
        continue;
      }
      let signatures = (typeParameter as TypeVariable).protocol1!.protocolDecl!.signaturesByNameWithTypeVariable(this.name, typeParameter as TypeVariable);
      if (signatures === null) {
        continue;
      }
      overload = resolveOverloadImpl(signatures!, this.typeArguments, this.argumentTypes, this.returnType);
      if (overload.func1 !== null) {
        break;
      }
      failures.concat(overload!.failures!);
      overload = null;
    }
    if (overload === null) {
      overload = resolveOverloadImpl(possibleOverloads!, this.typeArguments, this.argumentTypes, this.returnType!);
      if (overload!.func1 === null) {
        failures = failures.concat(overload!.failures!);
        let message = `{'Did not find function named'${this.name}' for call with '}`;
        if (this.typeArguments.length > 0) {
          message += `{'type arguments <'${this.typeArguments.map((typeArgument: Node) => typeArgument.toString()).join(';')}'> and '}`;
        }
        message += `{'argument types ('${this.argumentTypes.map((typeArgument: Node) => typeArgument.toString()).join(';')}')'}`;
        if (this.returnType != null) {
          message += `{' and return type '${this.returnType.toString()}}`;
        }
        if (failures.length > 0) {
          message += `{', but considered:\n'${failures.map((failure: OverloadResolutionFailure) => failure.toString()).join('\n')}')'}`;
        }
        throw new WTypeError(this.origin!.originString + message);
      }
    }
    for (let i = 0; i < typeArguments.length; ++i) {
      let typeArgumentType: Node = typeArguments[i];
      let typeParameter: Node = overload.func1!.typeParameters[i];
      if (!(typeParameter instanceof ConstexprTypeParameter)) {
        continue;
      }
      if (typeParameter.hmType!.equalsWithCommit(typeArgumentType) === null) {
        throw new Error(
          `At ${
            this.origin!.originString
          } constexpr type argument and parameter types not equal: argument = ${typeArgumentType.toString()}, parameter = ${typeParameter.hmType?.toString()}`
        );
      }
    }
    for (let i = 0; i < this.argumentTypes.length; ++i) {
      let argumentType: HmType = this.argumentTypes[i];
      let parameterType: Node = overload.func1!.parameters[i].hmType!.substituteToUnification(overload.func1!.typeParameters, overload.unificationContext!);
      let result: UnificationContext | null = argumentType.equalsWithCommit(parameterType);
      if (result === null) {
        throw new Error(
          `At ${
            this.origin!.originString
          } argument and parameter types not equal after type argument substitution: argument = ${argumentType.toString()}, parameter = ${parameterType.toString()}`
        );
      }
    }
    return this.resolveToOverload(overload);
  }

  resolveToOverload(overload: ResolveOverloadResult): HmType {
    this.func1 = overload.func1;
    this.actualTypeArguments = overload.typeArguments!.map((typeArgument: Node) =>
      typeArgument instanceof HmType ? (typeArgument.visit(new AutoWrapper()) as Node) : typeArgument
    );
    this.instantiatedActualTypeArguments = this.actualTypeArguments;
    let result = overload.func1!.returnType.substituteToUnification(overload.func1!.typeParameters, overload.unificationContext!);
    if (result === null) {
      throw new Error('Null return type');
    }
    result = result.visit(new AutoWrapper()) as HmType;
    this.resultType = result;
    return result;
  }

  becomeCast(returnType: HmType): void {
    this._returnType = new TypeRef(this.origin!, this.name, this._typeArguments);
    this._returnType.hmType = returnType;
    this.name = 'operator cast';
    this._isCast = true;
    this._typeArguments = [];
  }

  setCastData(returnType: TypeRef): void {
    this._returnType = returnType;
    this._isCast = true;
  }

  toString(): string {
    if (this.isBecome) {
      return `(${this.target.toString()})`;
    } else {
      let tmp = this.isCast ? `operator ${this.returnType!.toString()}` : this.name;
      let tmp2 =
        this.actualTypeArguments.length > 0 ? `<<${this.actualTypeArguments.map((actualTypeArgument: Node) => actualTypeArgument.toString).join(';')}>>` : '';
      return `${tmp} <${this.typeArguments.map((typeArgument: Node) => typeArgument.toString).join(';')}> ${tmp2}(${this.argumentList
        .map((argument: HmValue) => argument.toString)
        .join(';')})`;
    }
  }
}

class Checker extends Visitor {
  private _program: Program;
  private _currentStatement: Node | null = null;
  private _vertexEntryPoints: Set<string> = new Set<string>();
  private _fragmentEntryPoints: Set<string> = new Set<string>();

  constructor(program: Program) {
    super();
    this._program = program;
  }

  visitProgram(node: Node): void {
    let doStatement = (statement: Node): void => {
      this._currentStatement = statement;
      statement.visit(this);
    };
    for (let type of (node as Program).types.values()) {
      doStatement(type);
    }
    for (let protocol of (node as Program).protocols.values()) {
      doStatement(protocol);
    }
    for (let funcs of (node as Program).functions.values()) {
      for (let func1 of funcs as Func[]) {
        this.visitFunc(func1);
      }
    }
    for (let funcs of (node as Program).functions.values()) {
      for (let func1 of funcs as Func[]) {
        doStatement(func1);
      }
    }
  }

  checkShaderType(node: FuncDef): void {
    // FIXME: Relax these checks once we have implemented support for textures and samplers.
    if (node.typeParameters.length !== 0) {
      throw new WTypeError(`${node.origin!.originString}`, `Entry point ${node.name} must not have type arguments.`);
    }
    switch (node.shaderType) {
      case 'vertex':
        if (this._vertexEntryPoints.has(node.name)) {
          throw new WTypeError(`${node.origin!.originString}`, ` Duplicate vertex entry point name ${node.name}`);
        }
        this._vertexEntryPoints.add(node.name);
        break;
      case 'fragment':
        if (this._fragmentEntryPoints.has(node.name)) {
          throw new WTypeError(`${node.origin!.originString}`, ` Duplicate fragment entry point name ${node.name}`);
        }
        this._fragmentEntryPoints.add(node.name);
        break;

      default:
        break;
    }
  }

  checkOperatorOverload(func1: Func, resolveFuncs: (str: string) => Array<Func> | null): void {
    if (Lexer.textIsIdentifier(func1.name)) {
      return; // Not operator!
    }
    if (!func1.name.startsWith('operator')) {
      throw new Error(`Bad operator overload name: ${func1.name}`);
    }
    let typeVariableTracker = new TypeVariableTracker();
    for (let parameterType of func1.parameterTypes) {
      parameterType.visit(typeVariableTracker);
    }
    Node.visit(func1.returnTypeForOverloadResolution, typeVariableTracker);
    for (let typeParameter of func1.typeParameters) {
      if (!typeVariableTracker.set.has(typeParameter)) {
        throw new WTypeError(
          `${typeParameter.origin!.originString} Type parameter ${typeParameter} to operator ${func1.toDeclString()} is not inferrable from value parameters`
        );
      }
    }

    let checkGetter = (kind: string): void => {
      let numExpectedParameters = kind === 'index' ? WSL_2 : 1;
      if (func1.parameters.length !== numExpectedParameters) {
        throw new WTypeError(
          `${func1.origin!.originString} Incorrect number of parameters for ` +
            func1.name +
            ' (expected ' +
            numExpectedParameters +
            ', got ' +
            func1.parameters.length +
            ')'
        );
      }
      if ((func1.parameterTypes[0].unifyNode as HmType).isPtr) {
        throw new WTypeError(`${func1.origin!.originString} Cannot have getter for pointer type: ${func1.parameterTypes[0]}`);
      }
    };

    let checkSetter = (kind: string): void => {
      let numExpectedParameters = kind === 'index' ? WSL_3 : WSL_2;
      if (func1.parameters.length !== numExpectedParameters) {
        throw new WTypeError(
          `${func1.origin!.originString} Incorrect number of parameters for ${func1.name} (expected ${numExpectedParameters}, got ${func1.parameters.length})`
        );
      }
      if ((func1.parameterTypes[0].unifyNode as HmType).isPtr) {
        throw new WTypeError(`${func1.origin!.originString} Cannot have setter for pointer type: ${func1.parameterTypes[0].toString()}`);
      }
      if (func1.returnType.equals(func1.parameterTypes[0]) === null) {
        throw new WTypeError(
          `${
            func1.origin!.originString
          } First parameter type and return type of setter must match (parameter was ${func1.parameterTypes[0].toString()} but return was ${func1.returnType.toString()})`
        );
      }
      let valueType = func1.parameterTypes[numExpectedParameters - 1];
      let getterName: string = func1.name.substr(0, func1.name.length - 1);
      let getterFuncs: Func[] | null = resolveFuncs(getterName);
      if ((getterFuncs ?? null) === null) {
        throw new WTypeError(
          `${func1.origin!.originString} Every setter must have a matching getter, but did not find any function named ${getterName} to match ${func1.name}`
        );
      }
      let argumentTypes = func1.parameterTypes.slice(0, numExpectedParameters - 1);
      let overload: ResolveOverloadResult = resolveOverloadImpl(
        getterFuncs!,
        [],
        argumentTypes.map(($0) => $0 as Node),
        null
      );
      if (overload.func1 === null) {
        let failureStr = overload.failures!.map((failure: OverloadResolutionFailure) => {
          failure.toString();
        }).join('\n');
        let tmp = overload.failures!.length !== 0 ? `; tried:\n${failureStr}` : '';
        throw new WTypeError(`${func1.origin!.originString} Did not find function named ${func1.name} with arguments ${argumentTypes.toString()}${tmp}`);
      }
      let resultType = overload.func1!.returnType.substituteToUnification(overload.func1!.typeParameters, overload.unificationContext!);
      if (resultType.equals(valueType) === null) {
        throw new WTypeError(
          `${func1.origin!.originString} Setter and getter must agree on value type (getter at ${
            overload.func1.origin!.originString
          } says ${resultType.toString()} while this setter says ${valueType.toString()})`
        );
      }
    };

    let checkAnder = (kind: string): void => {
      let numExpectedParameters = kind === 'index' ? WSL_2 : 1;
      if (func1.parameters.length !== numExpectedParameters) {
        throw new WTypeError(
          `${func1.origin!.originString} Incorrect number of parameters for ${func1.name} (expected ${numExpectedParameters}, got ${func1.parameters.length})`
        );
      }
      if (!(func1.returnType.unifyNode as HmType).isPtr) {
        throw new WTypeError(`${func1.origin!.originString} Return type of ander is not a pointer: ${func1.returnType.toString()}`);
      }
      if (!(func1.parameterTypes[0].unifyNode as HmType).isRef) {
        throw new WTypeError(`${func1.origin!.originString} Parameter to ander is not a reference: ${func1.parameterTypes[0].toString()}`);
      }
    };

    switch (func1.name) {
      case 'operator cast':
        break;
      case 'operator++':
      case 'operator--':
        if (func1.parameters.length !== 1) {
          throw new WTypeError(`${func1.origin!.originString} Incorrect number of parameters for ${func1.name} (expected 1, got ${func1.parameters.length})`);
        }
        if (func1.parameterTypes[0].equals(func1.returnType) === null) {
          throw new WTypeError(
            `${func1.origin!.originString} Parameter type and return type must match for ${func1.name} (parameter is ${
              func1.parameterTypes[0]
            } while return is ${func1.returnType.toString()})`
          );
        }
        break;
      case 'operator+':
      case 'operator-':
        if (func1.parameters.length !== 1 && func1.parameters.length !== WSL_2) {
          throw new WTypeError(
            `${func1.origin!.originString} Incorrect number of parameters for ${func1.name} (expected 1 or 2, got ${func1.parameters.length})`
          );
        }
        break;
      case 'operator*':
      case 'operator/':
      case 'operator%':
      case 'operator&':
      case 'operator|':
      case 'operator^':
      case 'operator<<':
      case 'operator>>':
        if (func1.parameters.length !== WSL_2) {
          throw new WTypeError(`${func1.origin!.originString} Incorrect number of parameters for ${func1.name} (expected 2, got ${func1.parameters.length})`);
        }
        break;
      case 'operator~':
        if (func1.parameters.length !== 1) {
          throw new WTypeError(`${func1.origin!.originString} Incorrect number of parameters for ${func1.name} (expected 1, got ${func1.parameters.length})`);
        }
        break;
      case 'operator==':
      case 'operator<':
      case 'operator<=':
      case 'operator>':
      case 'operator>=':
        if (func1.parameters.length !== WSL_2) {
          throw new WTypeError(`${func1.origin!.originString} Incorrect number of parameters for ${func1.name} (expected 2, got ${func1.parameters.length})`);
        }
        if (func1.returnType.equals(this._program.intrinsics.bool!) === null) {
          throw new WTypeError(`${func1.origin!.originString} Return type of ${func1.name} must be bool but was ${func1.returnType.toString()}`);
        }
        break;
      case 'operator[]':
        checkGetter('index');
        break;
      case 'operator[]=':
        checkSetter('index');
        break;
      case 'operator&[]':
        checkAnder('index');
        break;
      default:
        if (func1.name.startsWith('operator.')) {
          if (func1.name.endsWith('=')) {
            checkSetter('dot');
          } else {
            checkGetter('dot');
          }
          break;
        }
        if (func1.name.startsWith('operator&.')) {
          checkAnder('dot');
          break;
        }
        throw new Error('Parser accepted unrecognized operator: ${func1.name}');
    }
  }

  visitFuncDef(node: Node): void {
    if ((node as FuncDef).shaderType) {
      this.checkShaderType(node as FuncDef);
    }
    this.checkOperatorOverload(node as FuncDef, (name: string): Array<Func> | null => {
      return this._program.functions.get(name) as Array<Func> | null;
    });
    node.body.visit(this);
  }

  visitNativeFunc(node: Node): void {}

  visitProtocolDecl(node: Node): void {
    let newNode = node as ProtocolDecl;
    for (let signature of newNode.signatures) {
      let typeVariableTracker: TypeVariableTracker = new TypeVariableTracker();
      for (let parameterType of signature.parameterTypes) {
        parameterType.visit(typeVariableTracker);
      }
      Node.visit(signature.returnTypeForOverloadResolution, typeVariableTracker);
      for (let typeParameter of signature.typeParameters) {
        if (!typeVariableTracker.set.has(typeParameter)) {
          throw new WTypeError(typeParameter.origin!.originString, 'Type parameter to protocol signature not inferrable from value parameters');
        }
      }
      if (!typeVariableTracker.set.has(newNode.typeVariable)) {
        throw new WTypeError(signature.origin!.originString, `Protocol's type variable (${newNode.name}) not mentioned in signature: ${signature.toString()}`);
      }
      this.checkOperatorOverload(signature, (name: string): Array<Func> | null => {
        return newNode.signaturesByName(name) as Array<Func> | null;
      });
    }
  }

  visitEnumType(node: Node): void {
    let newNode = node as EnumType;
    newNode.baseType.visit(this);

    let baseType = newNode.baseType.unifyNode as HmType;

    if (!baseType.isInt) {
      throw new WTypeError(newNode.origin!.originString, `Base type of enum is not an integer: ${newNode.baseType.toString()}`);
    }

    for (let member of newNode.members) {
      if (member.value == null) {
        continue;
      }

      let memberType = member.value!.visit(this) as HmType;
      if (baseType.equalsWithCommit(memberType) == null) {
        throw new WTypeError(
          member.origin!.originString,
          `Type of enum member ${
            member.value!.name
          } does not patch enum base type (member type is ${memberType.toString()}, enum base type is newNode.baseType.toString())`
        );
      }
    }

    let nextValue: number = (baseType as NativeType).defaultValue;
    for (let member of newNode.members) {
      if (member.value != null) {
        nextValue = (baseType as NativeType).successorValue((member.value!.unifyNode as GenericLiteral).valueForSelectedType);
        continue;
      }
      member.value = (baseType as NativeType).createLiteral(member.origin!, nextValue);
      nextValue = (baseType as NativeType).successorValue(nextValue);
    }

    let memberArray: Array<EnumMember> = Array.from(newNode.members);
    for (let i = 0; i < memberArray.length; ++i) {
      let member = memberArray[i];
      for (let j = i + 1; j < memberArray.length; ++j) {
        let otherMember = memberArray[j];
        if (
          (baseType as NativeType).valuesEqual(
            (member.value!.unifyNode as GenericLiteral).valueForSelectedType,
            (otherMember.value!.unifyNode as GenericLiteral).valueForSelectedType
          )
        ) {
          throw new WTypeError(
            otherMember.origin!.originString +
              `Duplicate enum member value (${member.name} has ${member.value!.toString()} while ${otherMember.name} has ${otherMember.value!.toString()})`
          );
        }
      }
    }

    let foundZero = false;
    for (let member of newNode.members) {
      if ((baseType as NativeType).valuesEqual((member.value!.unifyNode as GenericLiteral).valueForSelectedType, (baseType as NativeType).defaultValue)) {
        foundZero = true;
        break;
      }
    }
    if (!foundZero) {
      throw new WTypeError(newNode.origin!.originString, 'Enum does not have a member with the value zero');
    }
  }

  checkTypeArguments(origin: LexerToken, typeParameters: Array<Node>, typeArguments: Array<Node>): void {
    for (let i = 0; i < typeParameters.length; ++i) {
      let argumentIsType = typeArguments[i] instanceof HmType;
      let result = typeArguments[i].visit(this);
      if (argumentIsType) {
        let results: VerifyResult = (typeArguments[i] as HmType).inherits((typeParameters[i] as HmType).protocol1);
        if (!results.result) {
          throw new WTypeError(origin.originString, `Type argument does not inherit protocol: ${results.reason}`);
        }
      } else {
        if ((result as HmType).equalsWithCommit(typeParameters[i].hmType!) == null) {
          throw new WTypeError(origin.originString, 'Wrong type for constexpr');
        }
      }
    }
  }

  visitTypeRef(node: Node): void {
    if (node.hmType == null) {
      throw new Error(`Type reference without a type in checker: ${node.toString()} at ${node.origin!.toString()}`);
    }
    if (!(node.hmType instanceof StructType)) {
      node.hmType!.visit(this);
    }
    this.checkTypeArguments(node.origin!, node.hmType.typeParameters, (node as TypeRef).typeArguments!);
  }

  visitArrayType(node: Node): void {
    let newNode = node as ArrayType;
    newNode.elementType.visit(this);

    if (!newNode.numElements!.isConstexpr) {
      throw new WTypeError(newNode.origin!.originString, 'Array length must be constexpr');
    }

    let type: HmType = newNode.numElements!.visit(this) as HmType;

    if (type.equalsWithCommit(this._program.intrinsics.uint32!) == null) {
      throw new WTypeError(newNode.origin!.originString, 'Array length must be a uint32');
    }
  }

  visitVariableDecl2(node: Node): void {
    let newNode = node as VariableDecl;
    newNode.hmType!.visit(this);
    if (newNode.initializer != null) {
      let lhsType: HmType = newNode.hmType!;
      let rhsType = newNode.initializer.visit(this) as HmType;
      if (lhsType.equalsWithCommit(rhsType) == null) {
        throw new WTypeError(newNode.origin!.originString, `Type mismatch in variable initialization: ${lhsType.toString()} versus ${rhsType.toString()}`);
      }
    }
  }

  visitAssignment2(node: Node): HmType {
    let newNode = node as Assignment;
    let lhsType: HmType = newNode.lhs.visit(this) as HmType;
    if (!newNode.lhs.isLValue) {
      throw new WTypeError(newNode.origin!.originString, `LHS of assignment is not an LValue: ${newNode.lhs.toString()}${newNode.lhs.notLValueReasonString}`);
    }
    let rhsType: HmType = newNode.rhs.visit(this) as HmType;
    if (lhsType.equalsWithCommit(rhsType) == null) {
      throw new WTypeError(newNode.origin!.originString, `Type mismatch in assignment: ${lhsType.toString()} versus ${rhsType.toString()}`);
    }
    newNode.hmType = lhsType;
    return lhsType;
  }

  visitIdentityExpression2(node: Node): HmType {
    return (node as Expression).target.visit(this) as HmType;
  }

  visitReadModifyWriteExpression2(node: Node): TypeRef {
    let newNode = node as ReadModifyWriteExpression;
    let lhsType: TypeRef = newNode.lValue!.visit(this) as TypeRef;
    if (!newNode.lValue!.isLValue) {
      throw new WTypeError(
        newNode.origin!.originString,
        `LHS of read-modify-write is not an LValue: ${newNode.lValue!.toString()}${newNode.lValue!.notLValueReasonString}`
      );
    }
    newNode.oldValueVar.hmType = lhsType;
    newNode.newValueVar.hmType = lhsType;
    newNode.oldValueVar.visit(this);
    newNode.newValueVar.visit(this);
    let newValueType: TypeRef = newNode.newValueExp!.visit(this) as TypeRef;
    if (lhsType.equalsWithCommit(newValueType) === null) {
      throw new WTypeError(newNode.origin!.originString, `Type mismatch in read-modify-write: ${lhsType.toString()} versus ${newValueType.toString()}`);
    }
    return newNode.resultExp!.visit(this) as TypeRef;
  }

  visitAnonymousVariable2(node: Node): void {
    if ((node as AnonymousVariable).hmType == null) {
      throw new Error('Anonymous variable must know type before first appearance');
    }
  }

  visitDereferenceExpression2(node: Node): HmType {
    let newNode = node as DereferenceExpression;
    let type: HmType = (newNode.ptr.visit(this) as HmType).unifyNode as HmType;
    if (!type.isPtr) {
      throw new WTypeError(newNode.origin!.originString, `Type passed to dereference is not a pointer: ${type.toString()}`);
    }
    newNode.hmType = type.elementType;
    newNode.addressSpace = type.addressSpace;
    if (newNode.addressSpace == null) {
      throw new Error(`Null address space in type: ${type.toString()}`);
    }
    return newNode.hmType!;
  }

  visitMakePtrExpression2(node: Node): PtrType {
    let newNode = node as MakePtrExpression;
    let elementType: HmType = (newNode.lValue!.visit(this) as HmType).unifyNode as HmType;
    if (!newNode.lValue!.isLValue) {
      throw new WTypeError(
        newNode.origin!.originString,
        `Operand to & is not an LValue: ${newNode.lValue!.toString()}${newNode.lValue!.notLValueReasonString}`
      );
    }
    return new PtrType(newNode.origin, (newNode.lValue! as Expression).addressSpace!, elementType);
  }

  visitMakeArrayRefExpression2(node: Node): ArrayRefType {
    let newNode = node as MakeArrayRefExpression;
    let elementType: HmType = (newNode.lValue!.visit(this) as HmType).unifyNode as HmType;
    if (elementType.isPtr) {
      newNode.become(new ConvertPtrToArrayRefExpression(newNode.origin!, newNode.lValue as VariableRef));
      return new ArrayRefType(newNode.origin, elementType.addressSpace, elementType.elementType as TypeRef | null);
    }

    if (!newNode.lValue!.isLValue) {
      throw new WTypeError(
        newNode.origin!.originString,
        `Operand to @ is not an LValue: ${newNode.lValue!.toString()}${newNode.lValue!.notLValueReasonString}`
      );
    }

    if (elementType.isArray) {
      newNode.numElements = elementType.numElements;
      elementType = elementType.elementType!;
    } else {
      newNode.numElements = UintLiteral.withType(newNode.origin!, 1, this._program.intrinsics.uint32!);
    }
    return new ArrayRefType(newNode.origin, (newNode.lValue as Expression).addressSpace!, elementType);
  }

  visitConvertToArrayRefExpression(node: Node): void {
    throw new Error('Should not exist yet.');
  }

  finishVisitingPropertyAccess(node: PropertyAccessExpression, baseType: HmType, extraArgs: Array<Expression>, extraArgTypes: Array<HmType>): HmType {
    baseType = baseType.visit(new AutoWrapper()) as HmType;
    node.baseType = baseType;

    // Such a type must exist. This may throw if it doesn't.
    let typeForAnd: ReferenceType = baseType.argumentTypeForAndOverload(node.origin!) as ReferenceType;
    if (typeForAnd == null) {
      throw new Error('Cannot get typeForAnd');
    }
    let errorForGet: WTypeError = new WTypeError('', '');
    let errorForAnd: WTypeError = new WTypeError('', '');

    try {
      let tmpExtraArgs: Array<Expression> = [node.base];
      extraArgs.forEach((item) => {
        tmpExtraArgs.push(item);
      });

      let tmpExtraArgTypes: Array<HmType> = [baseType];
      extraArgTypes.forEach((item) => {
        tmpExtraArgTypes.push(item);
      });

      let result: CallResolveResult = CallExpression.resolve(
        node.origin!,
        node.possibleGetOverloads,
        (this._currentStatement! as FuncDef).typeParameters as Array<TypeVariable>,
        node.getFuncName,
        [],
        tmpExtraArgs,
        tmpExtraArgTypes,
        null
      );
      node.callForGet = result.call;
      node.resultTypeForGet = result.resultType;
    } catch (e) {
      if (!(e instanceof WTypeError)) {
        throw e as Error;
      }
      errorForGet = e as WTypeError;
    }

    try {
      let baseForAnd: Expression = baseType.argumentForAndOverload(node.origin!, node.base) as Expression;

      let tmpExtraArgs: Array<Expression> = [baseForAnd];
      extraArgs.forEach((item) => {
        tmpExtraArgs.push(item);
      });

      let tmpExtraArgTypes: Array<HmType> = [typeForAnd];
      extraArgTypes.forEach((item) => {
        tmpExtraArgTypes.push(item);
      });

      let result: CallResolveResult = CallExpression.resolve(
        node.origin!,
        node.possibleAndOverloads,
        (this._currentStatement! as FuncDef).typeParameters as Array<TypeVariable>,
        node.andFuncName,
        [],
        tmpExtraArgs,
        tmpExtraArgTypes,
        null
      );
      node.callForAnd = result.call;
      node.resultTypeForAnd = (result.resultType.unifyNode as PtrType).returnTypeFromAndOverload(node.origin!);
    } catch (e) {
      if (!(e instanceof WTypeError)) {
        throw e as Error;
      }
      errorForAnd = e as WTypeError;
    }

    if (node.resultTypeForGet === null && node.resultTypeForAnd === null) {
      throw new WTypeError(
        node.origin!.originString,
        `Cannot resolve access; tried by-value:\n${errorForGet.typeErrorMessage}\nand tried by-pointer:\n${errorForAnd.typeErrorMessage}`
      );
    }

    if (node.resultTypeForGet !== null && node.resultTypeForAnd !== null && node.resultTypeForGet.equals(node.resultTypeForAnd) === null) {
      throw new WTypeError(
        node.origin!.originString,
        `Result type resolved by-value (${node.resultTypeForGet!.toString()}) does not match result type resolved by-pointer (${node.resultTypeForAnd!.toString()})`
      );
    }

    try {
      let tmpExtraArgs: Array<Expression> = [node.base];
      extraArgs.forEach((item) => {
        tmpExtraArgs.push(item);
      });

      let tmpExtraArgTypes: Array<HmType> = [baseType];
      extraArgTypes.forEach((item) => {
        tmpExtraArgTypes.push(item);
      });
      tmpExtraArgTypes.push(node.resultType!);

      let result: CallResolveResult = CallExpression.resolve(
        node.origin!,
        node.possibleSetOverloads,
        (this._currentStatement! as FuncDef).typeParameters as TypeVariable[],
        node.setFuncName,
        [],
        tmpExtraArgs,
        tmpExtraArgTypes,
        null
      );
      node.callForSet = result.call;
      if (result.resultType.equals(baseType) == null) {
        throw new WTypeError(node.origin!.originString, `Result type of setter ${result!.call.func1!.toString()} is not the base type ${baseType.toString()}`);
      }
    } catch (e) {
      if (!(e instanceof WTypeError)) {
        throw e as Error;
      }
      node.errorForSet = e as WTypeError;
    }

    // OK, now we need to determine if we are an lvalue. We are an lvalue if we can be assigned to. We can
    // be assigned to if we have an ander or setter. But it's weirder than that. We also need the base to be
    // an lvalue, except unless the base is an array reference.
    if (node.callForAnd == null && node.callForSet == null) {
      node.isLValue = false;
      node.notLValueReason = `Have neither ander nor setter. Tried setter:\n${node.errorForSet!.typeErrorMessage}\n'and tried ander:\n${
        errorForAnd.typeErrorMessage
      }`;
    } else if (!node.base.isLValue && !baseType.isArrayRef) {
      node.isLValue = false;
      node.notLValueReason = 'Base of property access is neither a lvalue nor an array reference';
    } else {
      node.isLValue = true;
      node.addressSpace = node.base.isLValue ? node.base.addressSpace : baseType.addressSpace;
    }

    return node.resultType;
  }

  visitDotExpression2(node: Node): HmType {
    let newNode = node as DotExpression;
    let structType: HmType = (newNode.struct1!.visit(this) as HmType).unifyNode as HmType;
    return this.finishVisitingPropertyAccess(newNode, structType, [], []);
  }

  visitIndexExpression2(node: Node): HmType {
    let newNode = node as IndexExpression;
    let arrayType: HmType = (newNode.array.visit(this) as HmType).unifyNode as HmType;
    let indexType: HmType = newNode.index.visit(this) as HmType;
    return this.finishVisitingPropertyAccess(newNode, arrayType, [newNode.index], [indexType]);
  }

  visitVariableRef2(node: Node): HmType {
    let newNode = node as VariableRef;
    if (newNode.variable!.hmType === null) {
      throw new Error(`Variable has no type: ${newNode.variable!.toString()}`);
    }
    return newNode.variable!.hmType;
  }

  visitReturn(node: Node): void {
    let newNode = node as Return;
    if (newNode.value != null) {
      let resultType: HmType = newNode.value!.visit(this) as HmType;
      if (resultType == null) {
        throw new Error(`Null result type from ${newNode.value!.toString()}`);
      }

      if (newNode.func1!.returnType.equalsWithCommit(resultType as Node) == null) {
        throw new WTypeError(
          newNode.origin!.originString,
          `Trying to return ${resultType.toString()} in a function that returns ${newNode.func1!.returnType.toString()}`
        );
      }
      return;
    }

    if (newNode.func1!.returnType.equalsWithCommit(this._program.intrinsics.void!) == null) {
      throw new WTypeError(newNode.origin!.originString, 'Non-void function must return a value');
    }
  }

  visitGenericLiteral2(node: Node): HmType {
    return (node as GenericLiteral).hmType!;
  }

  visitNullLiteral2(node: Node): NullType {
    return (node as NullLiteral).hmType as NullType;
  }

  visitBoolLiteral2(node: Node): NativeType {
    return this._program.intrinsics.bool!;
  }

  visitEnumLiteral2(node: Node): EnumType {
    return (node as EnumLiteral).member.enumType!;
  }

  requireBool(expression: CallExpression): void {
    let type: TypeRef = expression.visit(this) as TypeRef;
    if (type == null) {
      throw new Error(`Expression has no type, but should be bool: ${expression.toString()}`);
    }

    if (type.equals(this._program.intrinsics.bool!) == null) {
      throw new WTypeError(`Expression isn't a bool: ${expression.toString()}`, '');
    }
  }

  visitLogicalNot2(node: Node): NativeType {
    this.requireBool((node as LogicalNot).operand as CallExpression);
    return this._program.intrinsics.bool!;
  }

  visitLogicalExpression2(node: Node): NativeType {
    let newNode = node as LogicalExpression;
    this.requireBool(newNode.left as CallExpression);
    this.requireBool(newNode.right as CallExpression);
    return this._program.intrinsics.bool!;
  }

  visitIfStatement(node: Node): void {
    let newNode = node as IfStatement;
    this.requireBool(newNode.conditional as CallExpression);
    newNode.body.visit(this);
    if (newNode.elseBody) {
      newNode.elseBody!.visit(this);
    }
  }

  visitWhileLoop(node: Node): void {
    let newNode = node as WhileLoop;
    this.requireBool(newNode.conditional as CallExpression);
    newNode.body.visit(this);
  }

  visitDoWhileLoop(node: Node): void {
    let newNode = node as DoWhileLoop;
    newNode.body.visit(this);
    this.requireBool(newNode.conditional as CallExpression);
  }

  visitForLoop(node: Node): void {
    let newNode = node as ForLoop;
    if (newNode.initialization) {
      newNode.initialization.visit(this);
    }
    if (newNode.condition != null) {
      this.requireBool(newNode.condition as CallExpression);
    }
    if (newNode.increment != null) {
      newNode.increment.visit(this);
    }
    newNode.body.visit(this);
  }

  visitSwitchStatement(node: Node): void {
    let newNode = node as SwitchStatement;
    let type: TypeRef = (newNode.value.visit(this) as TypeRef).commit();

    if (!(type.unifyNode as HmType).isInt && !(type.unifyNode instanceof EnumType)) {
      throw new WTypeError(newNode.origin!.originString, `Cannot switch on non-integer/non-enum type: ${type.toString()}`);
    }
    newNode.hmType = type;

    let hasDefault = false;

    for (let switchCase of newNode.switchCases) {
      switchCase.body.visit(this);

      if (switchCase.isDefault) {
        hasDefault = true;
        continue;
      }

      if (!switchCase.value!.isConstexpr) {
        throw new WTypeError(switchCase.origin!.originString, `Switch case not constexpr: ${switchCase.value!.toString()}`);
      }

      let caseType: HmType = switchCase.value!.visit(this) as HmType;
      if (type.equalsWithCommit(caseType) === null) {
        throw new WTypeError(
          switchCase.origin!.originString,
          `Switch case type does not match switch value type (case type is ${caseType} but switch value type is ${type.toString()})`
        );
      }
    }
    for (let i = 0; i < newNode.switchCases.length; ++i) {
      let firstCase: SwitchCase = newNode.switchCases[i];
      for (let j = i + 1; j < newNode.switchCases.length; ++j) {
        let secondCase: SwitchCase = newNode.switchCases[j];

        if (firstCase.isDefault !== secondCase.isDefault) {
          continue;
        }

        if (firstCase.isDefault) {
          throw new WTypeError(secondCase.origin!.originString, 'Duplicate default case in switch statement');
        }

        let a = (firstCase.value!.unifyNode as Expression).valueForSelectedType as number;
        let b = (secondCase.value!.unifyNode as Expression).valueForSelectedType as number;

        let valuesEqual: boolean = false;
        if (type.unifyNode instanceof EnumType) {
          valuesEqual = (type.unifyNode as EnumType).valuesEqual(a, b);
        }
        if (type.unifyNode instanceof NativeType) {
          valuesEqual = (type.unifyNode as NativeType).valuesEqual(a, b);
        }

        if (valuesEqual) {
          throw new WTypeError(
            secondCase.origin!.originString,
            `Duplicate case in switch statement for value ${(firstCase.value!.unifyNode as Expression).valueForSelectedType}`
          );
        }
      }
    }

    if (!hasDefault) {
      let includedValues: Set<number> = new Set();
      for (let switchCase of newNode.switchCases) {
        includedValues.add((switchCase.value!.unifyNode as Expression).valueForSelectedType);
      }

      for (let member of (type.unifyNode as HmType).allValues()) {
        if (!includedValues.has(member.value)) {
          throw new WTypeError(newNode.origin!.originString, `Value not handled by switch statement: ${member.name}`);
        }
      }
    }
  }

  visitCommaExpression(node: Node): Expression | null {
    let result: Expression | null = null;
    for (let expression of (node as CommaExpression).list) {
      result = expression.visit(this) as Expression;
    }
    return result;
  }

  visitCallExpression2(node: Node): HmType {
    let newNode = node as CallExpression;
    let typeArguments: Array<HmType> = newNode.typeArguments.map((typeArgument: Node): HmType => typeArgument.visit(this) as HmType) as Array<HmType>;
    let argumentTypes: Array<HmType> = newNode.argumentList.map((argument: Node): HmType => {
      let newArgument: HmType = argument.visit(this) as HmType;
      if (newArgument == null) {
        throw new Error(`visitor returned null for ${argument.toString()}`);
      }
      return newArgument.visit(new AutoWrapper()) as HmType;
    });

    newNode.argumentTypes = argumentTypes;
    if (newNode.returnType !== null) {
      newNode.returnType!.visit(this);
    }

    let result: HmType = newNode.resolve(
      newNode.possibleOverloads as Array<Func>,
      (this._currentStatement! as FuncDef).typeParameters as Array<TypeVariable>,
      typeArguments
    );
    return result;
  }

  mapReturn(): Map<string, (node: Base) => void | Base> {
    let methods = super.mapReturn() as Map<string, (node: Base) => void | Base>;
    methods.set('visitCallExpression', this.visitCallExpression2 as (node: Base) => HmType);
    methods.set('visitLogicalExpression', this.visitLogicalExpression2 as (node: Base) => NativeType);
    methods.set('visitLogicalNot', this.visitLogicalNot2 as (node: Base) => NativeType);
    methods.set('visitEnumLiteral', this.visitEnumLiteral2 as (node: Base) => EnumType);
    methods.set('visitBoolLiteral', this.visitBoolLiteral2 as (node: Base) => NativeType);
    methods.set('visitNullLiteral', this.visitNullLiteral2 as (node: Base) => NullType);
    methods.set('visitGenericLiteral', this.visitGenericLiteral2 as (node: Base) => HmType);
    methods.set('visitVariableRef', this.visitVariableRef2 as (node: Base) => HmType);
    methods.set('visitIndexExpression', this.visitIndexExpression2 as (node: Base) => HmType);
    methods.set('visitDotExpression', this.visitDotExpression2 as (node: Base) => HmType);
    methods.set('visitConvertToArrayRefExpression', this.visitConvertToArrayRefExpression as (node: Base) => void);
    methods.set('visitMakeArrayRefExpression', this.visitMakeArrayRefExpression2 as (node: Base) => ArrayRefType);
    methods.set('visitMakePtrExpression', this.visitMakePtrExpression2 as (node: Base) => PtrType);
    methods.set('visitDereferenceExpression', this.visitDereferenceExpression2 as (node: Base) => HmType);
    methods.set('visitReadModifyWriteExpression', this.visitReadModifyWriteExpression2 as (node: Base) => TypeRef);
    methods.set('visitIdentityExpression', this.visitIdentityExpression2 as (node: Base) => HmType);
    methods.set('visitAssignment', this.visitAssignment2 as (node: Base) => HmType);
    methods.set('visitVariableDecl', this.visitVariableDecl2 as (node: Base) => void);

    return methods;
  }
}

class CommaExpression extends Expression {
  private _list: Array<HmValue>;

  constructor(origin: LexerToken, list: Array<HmValue>) {
    super(origin);
    this._list = list;

    for (let expression of list) {
      if (expression === null) {
        throw new Error('null expression');
      }
    }
  }

  get list(): Array<HmValue> {
    return this._list;
  }

  // NOTE: It's super tempting to say that CommaExpression is an lValue if its last entry is an lValue. But,
  // PropertyResolver relies on this not being the case.
  toString(): string {
    return '(' + this.list.map((item) => item.toString()).join(',') + ')';
  }
}

class ConstexprFolder extends Visitor {
  visitCallExpression(node: Node): void {
    super.visitCallExpression(node);

    if (
      node.name === 'operator-' &&
      (node as CallExpression).typeArguments.length === 0 &&
      (node as CallExpression).argumentList.length === 1 &&
      ((node as CallExpression).argumentList[0].unifyNode as GenericLiteral).isConstexpr &&
      ((node as CallExpression).argumentList[0].unifyNode as GenericLiteral).negConstexpr !== null
    ) {
      (node as CallExpression).become(((node as CallExpression).argumentList[0].unifyNode as GenericLiteral).negConstexpr!());
      return;
    }
  }

  visitTypeOrVariableRef(node: Node): void {}
}

// ConstexprTypeParameter class
class ConstexprTypeParameter extends HmValue {
  constructor(origin: LexerToken, name: string, type: TypeRef) {
    super();
    this.origin = origin;
    this._name = name;
    this.hmType = type;
  }

  get isConstexpr(): boolean {
    return true;
  }

  get isUnifiable(): boolean {
    return true;
  }

  get varIsLValue(): boolean {
    return false;
  }

  typeVariableUnify(unificationContext: UnificationContext, other: HmValue): boolean {
    if (!(other instanceof HmValue)) {
      return false;
    }
    if (!this.hmType!.unify(unificationContext, other.hmType!)) {
      return false;
    }
    return this.typeVariableUnifyImpl(unificationContext, other!);
  }

  unifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    return this.typeVariableUnify(unificationContext, other as HmValue);
  }

  verifyAsArgument(unificationContext: UnificationContext): VerifyResult {
    return new VerifyResult(true);
  }

  verifyAsParameter(unificationContext: UnificationContext): VerifyResult {
    return new VerifyResult(true);
  }

  toString(): string {
    return `${this.hmType!.name} ${this.name}`;
  }
}

class Continue extends Node {
  constructor(origin: LexerToken) {
    super();
    this.origin = origin;
  }

  toString(): string {
    return 'Continue';
  }
}

class ConvertPtrToArrayRefExpression extends Expression {
  constructor(origin: LexerToken, lValue: VariableRef) {
    super(origin);
    this.lValue = lValue;
  }

  toString(): string {
    return `@(${this.lValue!.toString()})`;
  }
}

class DereferenceExpression extends Expression {
  private _ptr: HmValue;
  constructor(origin: LexerToken, ptr: HmValue, type: HmType | null = null, addressSpace: string | null = null) {
    super(origin);
    this._ptr = ptr;
    this.hmType = type;
    this.addressSpace = addressSpace;
  }

  get ptr(): HmValue {
    return this._ptr;
  }

  get isLValue(): boolean {
    if (this.isBecome) {
      return this.target.isLValue;
    } else {
      return true;
    }
  }

  toString(): string {
    if (this.isBecome) {
      return `(${this.target.toString()})`;
    } else {
      return `*(${this.ptr.toString()})`;
    }
  }
}

class DoWhileLoop extends Node {
  private _conditional: HmValue;

  constructor(origin: LexerToken, body: Block, conditional: HmValue) {
    super();
    this.origin = origin;
    this._conditional = conditional;
    this.body = body;
  }

  get conditional(): HmValue {
    return this._conditional;
  }

  toString(): string {
    return `do ${this.body!.toString()} while (${this.conditional.toString()});`;
  }
}

class DotExpression extends PropertyAccessExpression {
  fieldName: string = '';
  constructor(origin: LexerToken, struct1: Expression, fieldName: string) {
    super(origin, struct1);
    this.fieldName = fieldName;
  }

  get struct1(): Node {
    if (this.isBecome) {
      return super.struct1;
    } else {
      return this.base!;
    }
  }

  get getFuncName(): string {
    if (this.isBecome) {
      return super.getFuncName;
    } else {
      return `operator.${this.fieldName}`;
    }
  }

  get andFuncName(): string {
    if (this.isBecome) {
      return super.andFuncName;
    } else {
      return `operator&.${this.fieldName}`;
    }
  }

  get setFuncName(): string {
    if (this.isBecome) {
      return super.setFuncName;
    } else {
      return `operator.${this.fieldName}=`;
    }
  }

  toString(): string {
    if (this.isBecome) {
      return `(${this.target.toString()})`;
    } else {
      return `(${this.struct1.toString()}.${this.fieldName})`;
    }
  }
}

class EArrayRef {
  private _ptr: EPtr;
  private _length: number;

  constructor(ptr: EPtr, length: number) {
    this._ptr = ptr;
    this._length = length;
  }

  get ptr(): EPtr {
    return this._ptr;
  }

  get length(): number {
    return this._length;
  }

  toString(): string {
    return `A:<${this.ptr.toString()}, ${this.length}>`;
  }
}

class EBuffer {
  private _index: number;
  private _array: Array<null | number | boolean | EPtr | EArrayRef | EnumLiteral>;

  constructor(size: number) {
    if (!canAllocateEBuffers) {
      throw new Error('Trying to allocate EBuffer while allocation is disallowed');
    }
    this._index = eBufferCount + 1;
    this._array = new Array(size).fill(null);
  }

  static setCanAllocateEBuffers(value: boolean, callback: () => EPtr): EPtr | null {
    let oldCanAllocateEBuffers = canAllocateEBuffers;
    canAllocateEBuffers = value;
    try {
      return callback();
    } finally {
      canAllocateEBuffers = oldCanAllocateEBuffers;
    }
  }

  static disallowAllocation(callback: () => EPtr): EPtr | null {
    return EBuffer.setCanAllocateEBuffers(false, callback);
  }

  static allowAllocation(callback: () => EPtr): null | EPtr {
    return EBuffer.setCanAllocateEBuffers(true, callback);
  }

  eBufferGet(index: number): null | number | boolean | EPtr | EArrayRef | EnumLiteral {
    if (index < 0 || index >= this._array.length) {
      throw new Error(`Out of bounds buffer access (buffer = ${this.toString()}, index = ${index})`);
    }
    return this._array[index];
  }

  eBufferSet(index: number | null, value: null | number | boolean | EPtr | EArrayRef): void {
    if (index === null || index! < 0 || index! >= this._array.length) {
      throw new Error(`Out of bounds buffer access (buffer = ${this.toString()}, index = ${index})`);
    }
    this._array[index] = value;
  }

  get index(): number {
    return this._index;
  }

  toString(): string {
    return `B${this._index}:[${this._array}]`;
  }
}

class EBufferBuilder extends Visitor {
  _program: Program;

  constructor(program: Program) {
    super();
    this._program = program;
  }

  createEPtr(type: HmType): EPtr {
    if (type.size == null) {
      throw new Error(`Type does not have size: ${type}`);
    }
    let buffer: EBuffer = new EBuffer(type.size);
    if (type.populateDefaultValue == null) {
      throw new Error(`Cannot populateDefaultValue with: ${type.toString()}`);
    }
    type.populateDefaultValue(buffer, 0);
    return new EPtr(buffer, 0);
  }

  createEPtrForNode(node: HmValue): void {
    if (node.hmType == null) {
      throw new Error(`node has no type: ${node}`);
    }
    node.ePtr = this.createEPtr(node.hmType);
  }

  visitFuncParameter(node: Node): void {
    this.createEPtrForNode(node as HmValue);
  }

  visitVariableDecl(node: Node): void {
    let newNode = node as VariableDecl;
    this.createEPtrForNode(newNode);
    if (newNode.initializer != null) {
      newNode.initializer!.visit(this);
    }
  }

  visitFuncDef(node: Node): void {
    node.returnEPtr = this.createEPtr((node as FuncDef).returnType);
    super.visitFuncDef(node);
  }

  visitCallExpression(node: Node): void {
    node.resultEPtr = this.createEPtr((node as CallExpression).resultType as HmType);
    super.visitCallExpression(node);
  }

  visitMakePtrExpression(node: Node): void {
    node.ePtr = EPtr.box();
    super.visitMakePtrExpression(node);
  }

  visitGenericLiteral(node: Node): void {
    node.ePtr = EPtr.box();
  }

  visitNullLiteral(node: Node): void {
    node.ePtr = EPtr.box();
  }

  visitBoolLiteral(node: Node): void {
    node.ePtr = EPtr.box();
  }

  visitEnumLiteral(node: Node): void {
    node.ePtr = EPtr.box();
  }

  visitLogicalNot(node: Node): void {
    node.ePtr = EPtr.box();
    super.visitLogicalNot(node as LogicalNot);
  }

  visitLogicalExpression(node: Node): void {
    node.ePtr = EPtr.box();
    super.visitLogicalExpression(node as LogicalExpression);
  }

  visitAnonymousVariable(node: Node): void {
    this.createEPtrForNode(node as HmValue);
  }

  visitMakeArrayRefExpression(node: Node): void {
    let newNode = node as MakeArrayRefExpression;
    newNode.ePtr = EPtr.box();
    super.visitMakeArrayRefExpression(newNode);
  }

  visitConvertPtrToArrayRefExpression(node: Node): void {
    let newNode = node as ConvertPtrToArrayRefExpression;
    newNode.ePtr = EPtr.box();
    super.visitConvertPtrToArrayRefExpression(newNode);
  }
}

class EPtr extends Base {
  private _buffer: EBuffer;
  private _offset: number;

  constructor(buffer: EBuffer, offset: number) {
    super();
    if (offset === -1 || offset !== offset) {
      throw new Error(`Bad offset: ${offset}`);
    }
    this._buffer = buffer;
    this._offset = offset;
  }

  static box(value?: null | number | boolean | EPtr | EArrayRef | EnumLiteral | MakePtrExpression): EPtr {
    return new EPtr(new EBuffer(1), 0).box(value!);
  }

  box(value: null | number | boolean | EPtr | EArrayRef | EnumLiteral | MakePtrExpression): EPtr {
    this._buffer.eBufferSet(0, value as number | boolean);
    return this;
  }

  get buffer(): EBuffer {
    return this._buffer;
  }

  get offset(): number {
    return this._offset;
  }

  plus(offset: number): EPtr {
    return new EPtr(this.buffer, this.offset + offset);
  }

  loadValue(): null | number | boolean | EPtr | EArrayRef | EnumLiteral {
    return this.buffer.eBufferGet(this.offset);
  }

  ePtrGet(offset: number): null | number | boolean | EPtr | EArrayRef | EnumLiteral {
    return this.buffer.eBufferGet(this.offset + offset);
  }

  ePtrSet(offset: number, value: null | number | boolean | EPtr | EArrayRef): void {
    this.buffer.eBufferSet(this.offset + offset, value);
  }

  copyFrom(other: EPtr, size: number): void {
    if (size === -1) {
      throw new Error('Cannot copy null size');
    }
    let i = size;
    while (i > 0) {
      i -= 1;
      this.ePtrSet(i, other.ePtrGet(i) as number | boolean | EPtr | EArrayRef);
    }
  }

  toString(): string {
    return `B${this.buffer.index}:${this.offset}`;
  }
}

class EnumLiteral extends Expression {
  private _member: EnumMember | null = null;

  constructor(origin: LexerToken, member: EnumMember) {
    super(origin);
    this._member = member;
  }

  get member(): EnumMember {
    return this._member!;
  }

  get hmType(): HmType | null {
    return this.member.enumType;
  }

  set hmType(newValue: HmType | null) {
    this.member.enumType = newValue as EnumType;
  }

  get isConstexpr(): boolean {
    return true;
  }

  get valueForSelectedType(): number {
    return (this.member.value!.unifyNode as Expression).valueForSelectedType;
  }

  unifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    if (!(other instanceof EnumLiteral)) {
      return false;
    }
    return this.member === (other as EnumLiteral).member;
  }

  toString(): string {
    return `${this.member.enumType!.name}.${this.member.name}`;
  }
}

class EnumMember extends Node {
  value: Expression | null;
  enumType: EnumType | null = null;

  constructor(origin: LexerToken | null, name: string, value: Expression | null) {
    super();
    this.origin = origin;
    this.value = value;

    this.name = name;
  }

  toString(): string {
    let result = this.name;
    if (this.value !== null) {
      result += ` = ${this.value!.toString()}`;
    }
    return result;
  }
}

class EnumType extends HmType {
  private _baseType: HmType | null;
  private _members: Map<string, EnumMember> = new Map<string, EnumMember>();

  constructor(origin: LexerToken, name: string, baseType: HmType) {
    super();
    this.origin = origin;
    this._baseType = baseType;
    this._members = new Map<string, EnumMember>();
    this.name = name;
    
  }

  add(member: EnumMember): void {
    if (this._members.has(member.name)) {
      throw new WTypeError(`Type error at ${member.origin!.originString}: Duplicate enum member name: ${member.name}`);
    }
    member.enumType = this;
    this._members.set(member.name, member);
  }

  get baseType(): HmType {
    return this._baseType!;
  }

  memberByName(name: string): EnumMember | null {
    return this._members.get(name)!;
  }

  get members(): EnumMember[] {
    return Array.from(this._members.values());
  }
    
  get isPrimitive(): boolean {
    return true;
  }


  allValues(): ResultMemberInfo[] {
    let arr: ResultMemberInfo[] = [];
    for (let member of this.members) {
      arr.push(new ResultMemberInfo(Number((member.value?.unifyNode as GenericLiteral).valueForSelectedType), member.name));
    }
    return arr;
  }

  valuesEqual(a: number, b: number): boolean {
    return (this.baseType.unifyNode as NativeType).valuesEqual(a, b);
  }

  populateDefaultValue(buffer: EBuffer, offset: number) {
    this.baseType.populateDefaultValue(buffer, offset);
  }

  get size(): number {
    return this.baseType.size;
  }

  toString(): string {
    return `enum ${this.name} : ${this.baseType.toString()} { ${this.members.map(($0: EnumMember): string => $0.toString()).join(',')} }`;
  }
}

class Evaluator extends Visitor {
  _program: Program;

  constructor(program: Program) {
    super();
    this._program = program;
  }

  snapshot(type: HmType, dstPtr: EPtr | null, srcPtr: EPtr): EPtr {
    const size: number = type.size!;
    if (size === -1) {
      throw new Error(`Cannot get size of type: ${type.toString()} (size = ${size}, constructor = ${type.toString()})`);
    }
    let ptr: EPtr | null = dstPtr;
    if (ptr === null) {
      ptr = new EPtr(new EBuffer(size!), 0);
    }
    ptr!.copyFrom(srcPtr, size);
    return ptr!;
  }

  runFunc(func1: Func): EPtr | null {
    return EBuffer.disallowAllocation((): EPtr => this.runBody(func1.returnType, func1.returnEPtr, func1.body as Block)!);
  }

  runBody(type: HmType, ptr: EPtr | null, block: Block): EPtr | null {
    if (ptr === null) {
      throw new Error('Null ptr');
    }
    try {
      block.visit(this);
      // FIXME: We should have a check that there is no way to drop out of a function without
      // returning unless the function returns void.
      let value = assignmentEptr;
      assignmentEptr = null;
      return value;
    } catch (e) {
      let error = e as NormalError;
      if ((error != null && error?.message === breakException) || error?.message === continueException) {
        throw new Error('Should not see break/continue at function scope');
      }
      if (e instanceof ReturnException) {
        let result = this.snapshot(type, ptr, (e as ReturnException).value!);
        return result;
      }
      throw e as Error;
    }
  }

  visitFunctionLikeBlock2(node: Node): EPtr | null {
    let newNode = node as FunctionLikeBlock;
    for (let i = 0; i < newNode.argumentList.length; ++i) {
      newNode.parameters[i].ePtr!.copyFrom(newNode.argumentList[i].visit(this) as EPtr, newNode.parameters[i].hmType!.size!);
    }
    let result: EPtr | null = this.runBody(newNode.returnType, newNode.returnEPtr!, newNode.body as Block);
    return result;
  }

  visitReturn(node: Node): void {
    let newNode = node as Return;
    throw new ReturnException(newNode.value != null ? (newNode.value!.visit(this) as EPtr) : null);
  }

  visitVariableDecl(node: Node): void {
    let newNode = node as VariableDecl;
    if (newNode.ePtr!.buffer == null) {
      throw new Error('eptr without buffer in ' + newNode.toString());
    }
    newNode.hmType!.populateDefaultValue(newNode.ePtr!.buffer, newNode.ePtr!.offset!);
    if (newNode.initializer != null) {
      newNode.ePtr!.copyFrom(newNode.initializer!.visit(this) as EPtr, newNode.hmType!.size!);
    }
  }

  visitAssignment2(node: Node): EPtr {
    let newNode = node as Assignment;
    let target = newNode.lhs.visit(this) as EPtr;
    let source = newNode.rhs.visit(this) as EPtr;
    target.copyFrom(source, newNode.hmType!.size!);
    assignmentEptr = target;
    return target;
  }

  visitIdentityExpression2(node: Node): EPtr {
    return (node as IdentityExpression).target.visit(this) as EPtr;
  }

  visitDereferenceExpression2(node: Node): EPtr {
    let newNode = node as DereferenceExpression;
    let ptr: EPtr = (newNode.ptr.visit(this) as EPtr).loadValue() as EPtr;
    if (ptr === null) {
      throw new WTrapError(newNode.origin!.originString, 'Null dereference');
    }
    return ptr!;
  }

  visitMakePtrExpression2(node: Node): EPtr {
    let newNode = node as MakePtrExpression;
    let ptr: EPtr = newNode.lValue!.visit(this) as EPtr;
    return newNode.ePtr!.box(ptr);
  }

  visitMakeArrayRefExpression2(node: Node): EPtr {
    let newNode = node as MakeArrayRefExpression;
    return newNode.ePtr!.box(new EArrayRef(newNode.lValue!.visit(this) as EPtr, (newNode.numElements!.visit(this) as EPtr).loadValue() as number));
  }

  visitConvertPtrToArrayRefExpression2(node: Node): EPtr {
    let newNode = node as ConvertPtrToArrayRefExpression;
    return newNode.ePtr!.box(new EArrayRef((newNode.lValue!.visit(this) as EPtr).loadValue() as EPtr, 1));
  }

  visitCommaExpression2(node: Node): EPtr | null {
    let newNode = node as CommaExpression;
    let result: EPtr | null = null;
    for (let expression of newNode.list) {
      result = expression.visit(this) as EPtr | null;
    }
    return result;
  }

  visitVariableRef2(node: Node): EPtr {
    return (node as VariableRef).variable!.ePtr!;
  }

  visitGenericLiteral2(node: Node): EPtr {
    let newNode = node as GenericLiteral;
    let eptr = newNode.ePtr!.box(newNode.valueForSelectedType);
    assignmentEptr = eptr;
    return eptr;
  }

  visitNullLiteral2(node: Node): EPtr {
    return (node as NullLiteral).ePtr!.box(null);
  }

  visitBoolLiteral2(node: Node): EPtr {
    let newNode = node as BoolLiteral;
    return newNode.ePtr!.box(newNode.value);
  }

  visitEnumLiteral2(node: Node): EPtr {
    let newNode = node as EnumLiteral;
    return newNode.ePtr!.box((newNode.member.value!.unifyNode as Expression).valueForSelectedType);
  }

  visitLogicalNot2(node: Node): EPtr {
    let newNode = node as LogicalNot;
    let result = !(newNode.operand.visit(this) as EPtr).loadValue() as boolean;
    return newNode.ePtr!.box(result);
  }

  visitLogicalExpression2(node: Node): EPtr {
    let newNode = node as LogicalExpression;
    let lhs: boolean = (newNode.left.visit(this) as EPtr).loadValue() as boolean;
    let rhs: boolean = (newNode.right.visit(this) as EPtr).loadValue() as boolean;
    let result: boolean | null;
    switch (newNode.text) {
      case '&&':
        result = lhs && rhs;
        break;
      case '||':
        result = lhs || rhs;
        break;
      default:
        throw new Error('Unknown type of logical expression');
    }
    return newNode.ePtr!.box(result);
  }

  visitIfStatement2(node: Node): EPtr | null {
    let newNode = node as IfStatement;
    if ((newNode.conditional.visit(this) as EPtr).loadValue() as boolean) {
      return newNode.body.visit(this) as EPtr;
    } else if (newNode.elseBody) {
      return newNode.elseBody!.visit(this) as EPtr;
    } else {
      return null;
    }
  }

  visitWhileLoop(node: Node): void {
    let newNode = node as WhileLoop;
    while ((newNode.conditional.visit(this) as EPtr).loadValue()) {
      try {
        newNode.body.visit(this);
      } catch (e) {
        let error = e as NormalError;
        if (error.message === breakException) {
          break;
        }
        if (error.message === continueException) {
          continue;
        }
        throw e as Error;
      }
    }
  }

  visitDoWhileLoop(node: Node): void {
    let newNode = node as DoWhileLoop;

    do {
      try {
        newNode.body.visit(this);
      } catch (e) {
        let error = e as NormalError;
        if (error.message === breakException) {
          break;
        }
        if (error.message === continueException) {
          continue;
        }
        throw e as Error;
      }
    } while ((newNode.conditional.visit(this) as EPtr).loadValue() as boolean);
  }

  visitForLoop(node: Node): void {
    let newNode = node as ForLoop;
    newNode.initialization != null ? newNode.initialization!.visit(this) : true;
    while (newNode.condition != null ? ((newNode.condition!.visit(this) as EPtr).loadValue() as Boolean) : true) {
      try {
        newNode.body.visit(this);
      } catch (e) {
        let error = e as NormalError;
        if (error.message === breakException) {
          break;
        }
        if (error.message === continueException) {
          newNode.increment != null ? newNode.increment!.visit(this) : true;
          continue;
        }
        throw e as Error;
      }
      newNode.increment != null ? newNode.increment!.visit(this) != null : true;
    }
  }

  visitSwitchStatement(node: Node): boolean | void {
    let newNode = node as SwitchStatement;
    let findAndRunCast = (predicate: (switchCase: SwitchCase) => boolean): boolean => {
      for (let i = 0; i < newNode.switchCases.length; ++i) {
        const switchCase: SwitchCase = newNode.switchCases[i];
        if (predicate(switchCase)) {
          try {
            for (let j = i; j < newNode.switchCases.length; ++j) {
              newNode.switchCases[j].visit(this);
            }
          } catch (e) {
            let error = e as NormalError;
            if (error != null && error?.message !== breakException) {
              throw e as Error;
            }
          }
          return true;
        }
      }
      return false;
    };

    const value: number = (newNode.value.visit(this) as EPtr).loadValue() as number;

    let found = findAndRunCast((switchCase): boolean => {
      if (switchCase.isDefault) {
        return false;
      }

      if (newNode.hmType!.unifyNode instanceof EnumType) {
        return (newNode.hmType!.unifyNode as EnumType).valuesEqual(value, (switchCase.value!.unifyNode as Expression).valueForSelectedType);
      } else {
        return (newNode.hmType!.unifyNode as NativeType).valuesEqual(value, (switchCase.value!.unifyNode as Expression).valueForSelectedType);
      }
    });
    if (found) {
      return;
    }

    found = findAndRunCast((switchCase: SwitchCase): boolean => switchCase.isDefault);
    if (!found) {
      throw new Error('Switch statement did not find case');
    }
  }

  visitBreak(node: Node): void {
    throw new NormalError(breakException);
  }

  visitContinue(node: Node): void {
    throw new NormalError(continueException);
  }

  visitTrapStatement(node: Node): void {
    throw new WTrapError(node.origin!.originString, 'Trap statement');
  }

  visitAnonymousVariable(node: Node): void {
    node.hmType!.populateDefaultValue(node.ePtr!.buffer, node.ePtr!.offset!);
  }

  visitCallExpression2(node: Node): EPtr {
    let newNode = node as CallExpression;
    let callArguments: Array<() => EPtr> = Array<() => EPtr>();
    for (let i = 0; i < newNode.argumentList.length; ++i) {
      let argument: HmValue = newNode.argumentList[i];
      let type: HmType = newNode.nativeFuncInstance!.parameterTypes[i];
      if (type === null || argument === null) {
        throw new Error(`Cannot get type or argument; i = ${i}, argument = ${argument.toString()}, type = ${type.toString()}; in ${newNode.toString()}`);
      }
      let argumentValue: EPtr | null = argument.visit(this) as EPtr | null;
      if (argumentValue === null) {
        throw new Error(`Null argument value, i = ${i}, node = ${newNode.toString()}`);
      }
      callArguments.push((): EPtr => {
        let result: EPtr = this.snapshot(type, null, argumentValue!);
        return result;
      });
    }

    let result: EPtr = EBuffer.allowAllocation((): EPtr => {
      return (newNode.func1 as NativeFunc).implementation!(
        callArguments.map((thunk): EPtr => thunk()),
        newNode
      ) as EPtr;
    }) as EPtr;

    result = this.snapshot(newNode.nativeFuncInstance!.returnType, newNode.resultEPtr, result!);
    return result;
  }

  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    let methods: Map<string, (node: Base) => void | Base | string | null> = super.mapReturn() as Map<string, (node: Base) => void | Base | string | null>;
    methods.set('visitCallExpression', this.visitCallExpression2 as (node: Base) => EPtr);
    methods.set('visitIfStatement', this.visitIfStatement2 as (node: Base) => EPtr);
    methods.set('visitLogicalExpression', this.visitLogicalExpression2 as (node: Base) => EPtr);
    methods.set('visitLogicalNot', this.visitLogicalNot2 as (node: Base) => EPtr);
    methods.set('visitEnumLiteral', this.visitEnumLiteral2 as (node: Base) => EPtr);
    methods.set('visitBoolLiteral', this.visitBoolLiteral2 as (node: Base) => EPtr);
    methods.set('visitNullLiteral', this.visitNullLiteral2 as (node: Base) => EPtr);
    methods.set('visitGenericLiteral', this.visitGenericLiteral2 as (node: Base) => EPtr);
    methods.set('visitVariableRef', this.visitVariableRef2 as (node: Base) => EPtr);
    methods.set('visitCommaExpression', this.visitCommaExpression2 as (node: Base) => EPtr);
    methods.set('visitConvertPtrToArrayRefExpression', this.visitConvertPtrToArrayRefExpression2 as (node: Base) => EPtr);
    methods.set('visitMakeArrayRefExpression', this.visitMakeArrayRefExpression2 as (node: Base) => EPtr);
    methods.set('visitMakePtrExpression', this.visitMakePtrExpression2 as (node: Base) => EPtr);
    methods.set('visitDereferenceExpression', this.visitDereferenceExpression2 as (node: Base) => EPtr);
    methods.set('visitIdentityExpression', this.visitIdentityExpression2 as (node: Base) => EPtr);
    methods.set('visitFunctionLikeBlock', this.visitFunctionLikeBlock2 as (node: Base) => EPtr);
    methods.set('visitAssignment', this.visitAssignment2 as (node: Base) => EPtr);
    return methods;
  }
}
class ExpressionFinder extends Visitor {
  callback: (node: Node) => void;
  constructor(callback: (node: Node) => void) {
    super();
    this.callback = callback;
  }

  visitFunc(node: Node): void {
    let newNode = node as Func;
    this.callback(newNode.returnType);
    for (let typeParameter of newNode.typeParameters) {
      typeParameter.visit(this);
    }
    for (let parameter of newNode.parameters) {
      parameter.visit(this);
    }
  }

  visitFuncParameter(node: Node): void {
    this.callback(node.hmType!);
  }

  visitConstexprTypeParameter(node: Node): void {
    this.callback(node.hmType!);
  }

  visitAssignment(node: Node): void {
    this.callback(node);
  }

  visitReadModifyWriteExpression(node: Node): void {
    this.callback(node as ReadModifyWriteExpression);
  }

  visitIdentityExpression(node: Node): void {
    this.callback(node as Expression);
  }

  visitCallExpression(node: Node): void {
    this.callback(node);
  }

  visitReturn(node: Node): void {
    let newNode = node as Return;
    if (newNode.value != null) {
      this.callback(newNode.value!);
    }
  }

  visitWhileLoop(node: Node): void {
    let newNode = node as WhileLoop;
    this.callback(newNode.conditional);
    newNode.body.visit(this);
  }

  visitDoWhileLoop(node: Node): void {
    let newNode = node as DoWhileLoop;
    newNode.body.visit(this);
    this.callback(newNode.conditional);
  }

  visitIfStatement(node: Node): void {
    let newNode = node as IfStatement;
    this.callback(newNode.conditional);
    newNode.body.visit(this);
    if (newNode.elseBody != null) {
      newNode.elseBody.visit(this);
    }
  }

  visitForLoop(node: Node): void {
    // Initialization is a statement, not an expression. If it's an assignment or variableDecl, we'll
    // catch it and redirect to the callback.
    let newNode = node as ForLoop;
    if (newNode.initialization != null) {
      newNode.initialization.visit(this);
    }
    if (newNode.condition != null) {
      this.callback(newNode.condition!);
    }
    if (newNode.increment != null) {
      this.callback(newNode.increment!);
    }
    newNode.body.visit(this);
  }

  visitVariableDecl(node: Node): void {
    let newNode = node as VariableDecl;
    this.callback(newNode.hmType!);
    if (newNode.initializer != null) {
      this.callback(newNode.initializer!);
    }
  }
}
class Field extends Node {
  offset: number = -1;
  constructor(origin: LexerToken, name: string, type: HmType) {
    super();
    this.origin = origin;
    this.name = name;
    this.hmType = type;
  }

  toString(): string {
    return `${this.hmType!.toString()} ${this.name}`;
  }
}
class ForLoop extends Node {
  constructor(origin: LexerToken, initialization: Expression | null, condition: HmValue | null, increment: Expression | null, body: Node) {
    super();
    this.origin = origin;
    this.initialization = initialization;
    this.condition = condition;
    this.increment = increment;
    this.body = body;
  }

  toString(): string {
    return `for (${this.initialization != null ? this.initialization!.toString() : ' '}; ${this.condition != null ? this.condition!.toString() : ''}; ${
      this.increment != null ? this.increment!.toString() : ''
    }) ${this.body}`;
  }
}

class Func extends Node {
  protocolDecl: ProtocolDecl | null;
  _returnType: HmType = new HmType();
  _typeParameters: Array<Node> = Array<Node>();
  _parameters: Array<FuncParameter> = Array<FuncParameter>();
  private _isCast: boolean = false;
  private _shaderType: string | null;
  isRestricted: boolean = false;
  inlined: boolean = false;

  uninstantiatedReturnType: HmType = new HmType();

  constructor(
    origin: LexerToken,
    name: string,
    returnType: HmType,
    typeParameters: Array<Node>,
    parameters: Array<FuncParameter>,
    isCast: boolean,
    shaderType: string | null
  ) {
    super();
    for (let parameter of parameters!) {
      if (parameter == null) {
        throw new Error('Null parameter');
      }
      if (parameter.hmType == null) {
        throw new Error('Null parameter type');
      }
    }
    this.origin = origin;
    this.protocolDecl = new ProtocolDecl(origin, name);
    this._returnType = returnType;
    this._typeParameters = typeParameters;
    this._parameters = parameters;
    this._isCast = isCast;
    this._shaderType = shaderType;
    this.uninstantiatedReturnType = returnType;
    this.name = name;
  }
  get returnType(): HmType {
    return this._returnType;
  }

  get typeParameters(): Array<Node> {
    return this._typeParameters!;
  }

  set typeParameters(newValue: Array<Node>) {
    this._typeParameters = newValue;
  }

  get typeParametersForCallResolution(): Array<Node> {
    return this.typeParameters;
  }

  get parameters(): Array<FuncParameter> {
    return this._parameters!;
  }

  get parameterTypes(): Array<HmType> {
    return this.parameters.map((parameter: FuncParameter): HmType => {
      return parameter.hmType!;
    });
  }

  get isCast(): boolean {
    return this._isCast;
  }

  get shaderType(): string | null {
    return this._shaderType;
  }

  get returnTypeForOverloadResolution(): HmType | null {
    return this.isCast ? this.returnType : null;
  }

  get kind(): string {
    return 'Func';
  }

  toDeclString(): string {
    let result = '';
    if (this.shaderType != null) {
      result += this.shaderType + ' ';
    }
    if (this.isCast) {
      result += 'operator<' + this.typeParameters.map($0 => $0.toString()).join(",") + '> ' + this.returnType.toString();
    } else {
      result += this.returnType.toString() + ' ' + this.name + '<' + this.typeParameters.map($0 => $0.toString()).join(",") + '>';
    }
    return result + '(' + this.parameters.map($0 => $0.toString()).join(",") + ')';
  }

  toString(): string {
    return this.toDeclString();
  }
}
class FuncDef extends Func {
  constructor(
    origin: LexerToken,
    name: string,
    returnType: HmType,
    typeParameters: Array<Node>,
    parameters: Array<FuncParameter>,
    body: Block,
    isCast: boolean,
    shaderType: null | string
  ) {
    super(origin, name, returnType, typeParameters, parameters, isCast, shaderType);
    this.body = body;
    this.isRestricted = false;
  }

  rewrite(rewriter: Inliner): void {
    if (this._typeParameters!.length > 0) {
      throw new Error('Cannot rewrite an uninstantiated function');
    }
    this._returnType = this._returnType.visit(rewriter) as HmType;
    this._parameters = this._parameters!.map((parameter: FuncParameter): FuncParameter => {
      let tempPar = parameter.visit(rewriter) as FuncParameter;
      return tempPar;
    });
    this.body = this.body!.visit(rewriter) as Block;
  }

  toString(): string {
    return `${super.toString()} {$this.body.toString()}`;
  }
}

class FuncInstantiator {
  private _program: Program;
  private _instances: Map<Func, Array<ResultingFunc>>;

  constructor(program: Program) {
    this._program = program;
    this._instances = new Map<Func, Array<ResultingFunc>>();
  }

  // Returns a Func object that uniquely identifies a particular system of type arguments. You must
  // intantiate things with concrete types, because this code casually assumes this. Note that this
  // will return a different func from `func` no matter what. This ensures that we can use the
  // returned func for rewrites that instantiate types without destroying our ability to do overload
  // resolutions on the original Program.
  getUnique(func1: Func, typeArguments: Array<Node>): Func {
    let findTypeVar: FindTypeVariable = new FindTypeVariable(func1, typeArguments);
    for (let typeArgument of typeArguments) {
      typeArgument.visit(findTypeVar);
    }
    let instances = this._instances.get(func1);
    if (instances == null) {
      instances = [];
      this._instances.set(func1, instances);
    }
    for (let instance of instances!) {
      let ok = true;
      let i = instance.typeArguments.length;
      while (i > 0) {
        i -= 1;
        if (instance.typeArguments[i].equals(typeArguments[i]) == null) {
          ok = false;
          break;
        }
      }
      if (!ok) {
        continue;
      }
      return instance.func1;
    }
    let substitution = new InstantiationSubstitution(this, func1.typeParameters, typeArguments);
    let instantiateImmediates = new InstantiationInstantiateImmediates();
    let instantiate = new Instantiate(substitution, instantiateImmediates);
    let resultingFunc = func1.visit(instantiate) as Func;
    resultingFunc.uninstantiatedReturnType = func1.returnType.visit(substitution) as HmType;
    let instance = new ResultingFunc(resultingFunc, typeArguments);
    instances!.push(instance);
    this._instances.set(func1, instances!);
    return resultingFunc;
  }
}
class FuncParameter extends HmValue {
  // The name is optional. It's OK for it to be null!
  constructor(origin: LexerToken, name: string | null, type: HmType) {
    super();
    this.origin = origin;
    this._name = name ?? '';
    this._hmType = type;
  }

  get varIsLValue(): boolean {
    return true;
  }

  toString(): string {
    if (this.name === '') {
      return `${this.hmType!.toString()}`;
    }
    return `${this.hmType!.toString()} ${this.name}`;
  }
}
class FunctionLikeBlock extends HmValue {
  private _returnType: HmType = new HmType();
  private _argumentList: Array<Node> = [];
  private _parameters: Array<FuncParameter> = [];

  constructor(origin: LexerToken, returnType: HmType, argumentList: Array<Node>, parameters: Array<FuncParameter>, body: Block) {
    super();
    this.origin = origin;
    this._returnType = returnType;
    this._argumentList = argumentList;
    this._parameters = parameters;
    this._body = body;
  }

  get returnType(): HmType {
    return this._returnType;
  }

  get argumentList(): Array<Node> {
    return this._argumentList;
  }

  get parameters(): Array<FuncParameter> {
    return this._parameters;
  }

  resultType: Node = new Node();

  toString(): string {
    return `([&] (${this.parameters}) -> ${this.returnType.toString()} { ${this.body.toString()} }(${this.argumentList}))`;
  }
}

class HighZombieFinder extends Visitor {
  found(node: Node): void {
    throw new Error(`${node.origin!.originString}: High zombie: ${node.toString()}`);
  }

  visitDotExpression(node: Node): void {
    this.found(node);
  }

  visitIndexExpression(node: Node): void {
    this.found(node);
  }

  visitCallExpression(node: Node): void {
    super.visitCallExpression(node as CallExpression);
  }
}

class IdentityExpression extends Expression {
  constructor(target: HmValue) {
    super(target.origin!);
    this.target = target;
  }

  get unifyNode(): Node {
    return this.target.unifyNode;
  }

  get isConstexpr(): boolean {
    return this.target.isConstexpr;
  }

  get isLValue(): boolean {
    return this.target.isLValue;
  }

  get addressSpace(): string | null {
    return this.target.addressSpace;
  }
  set addressSpace(newValue: string | null) {
    super.addressSpace = newValue;
  }

  toString(): string {
    return `(${this.target.toString()})`;
  }
}

class IfStatement extends Node {
  private _conditional: HmValue;
  private _elseBody: Node | null;

  constructor(origin: LexerToken, conditional: HmValue, body: Node, elseBody: Node | null) {
    super();
    this.origin = origin;
    this._conditional = conditional;
    this._elseBody = elseBody;
    this.body = body;
  }

  get conditional(): HmValue {
    return this._conditional;
  }

  get elseBody(): Node | null {
    return this._elseBody;
  }

  toString(): string {
    let result = `if (${this.conditional.toString()}) ${this.body.toString()}`;
    if (this.elseBody != null) {
      return `${result} else ${this.elseBody!.toString()}`;
    }
    return result;
  }
}

class IndexExpression extends PropertyAccessExpression {
  index: Expression = new Expression(null);

  constructor(origin: LexerToken, array: Expression, index: Expression) {
    super(origin, array);
    this.index = index;
  }

  get array(): Expression {
    return this.base;
  }

  get getFuncName(): string {
    {
      if (this.isBecome) {
        return super.getFuncName;
      } else {
        return 'operator[]';
      }
    }
  }

  get andFuncName(): string {
    {
      if (this.isBecome) {
        return super.andFuncName;
      } else {
        return 'operator&[]';
      }
    }
  }

  get setFuncName(): string {
    {
      if (this.isBecome) {
        return super.setFuncName;
      } else {
        return 'operator[]=';
      }
    }
  }

  updateCalls(): void {
    if (this.isBecome) {
      super.updateCalls();
    } else {
      if (this.callForGet != null) {
        this.callForGet.argumentList[1] = this.index;
      }
      if (this.callForAnd != null) {
        this.callForAnd.argumentList[1] = this.index;
      }
      if (this.callForSet != null) {
        this.callForSet.argumentList[1] = this.index;
      }
      super.updateCalls();
    }
  }

  toString(): string {
    if (this.isBecome) {
      return `(${this.target.toString()})`;
    } else {
      return `(this.array.toString()[${this.index.toString()}])`;
    }
  }
}

class Inliner extends Rewriter {
  private _program: Program;
  private _visiting: VisitingSet;

  constructor(program: Program, func1: FuncDef, visiting: VisitingSet) {
    super();
    this._program = program;
    this._visiting = visiting;
  }

  visitCallExpression2(node: Node): HmValue {
    let newNode = node as CallExpression;
    let result: CallExpression | null = super.visitCallExpression(newNode);
    if (result != null && result!.nativeFuncInstance != null) {
      return result!;
    }
    return this._visiting.doVisit(newNode.func1 as Func, (): FunctionLikeBlock => {
      let func1: Func = this._program.funcInstantiator.getUnique(result!.func1 as Func, result!.actualTypeArguments!);
      if (func1.isNative) {
        throw new Error(`Unexpected native func: ${func1.toString()}`);
      }
      inlineFunction(this._program, func1, this._visiting);
      let resultingBlock: FunctionLikeBlock = new FunctionLikeBlock(
        result!.origin!,
        func1.returnType,
        result!.argumentList,
        func1.parameters,
        func1.body as Block
      );
      resultingBlock.returnEPtr = result!.resultEPtr;
      return resultingBlock;
    });
  }

  mapReturn(): Map<string, (node: Base) => void | Base | null> {
    let methods = super.mapReturn() as Map<string, (node: Base) => void | Base | null>;
    methods.set('visitCallExpression', this.visitCallExpression2 as (node: Base) => HmValue);
    return methods;
  }
}

class InstantiateImmediates extends Rewriter {
  visitTypeRef(node: Node): TypeRef {
    let newNode: TypeRef = super.visitTypeRef(node)!;
    if (newNode.hmType instanceof TypeRef || newNode.hmType instanceof ArrayType || newNode.hmType instanceof EnumType) {
      if (newNode.typeArguments!.length > 0) {
        throw new Error(`type does not support instantiation: ${node.hmType} (${node.hmType!.toString()})`);
      }
      return newNode;
    }
    if (newNode.hmType instanceof NativeType) {
      return (newNode.hmType as NativeType).instantiate(newNode.typeArguments as TypeVariable[])!.visit(new AutoWrapper()) as TypeRef;
    } else {
      return (newNode.hmType as StructType).instantiate(newNode.typeArguments)!.visit(new AutoWrapper()) as TypeRef;
    }
  }

  visitReferenceType(node: ReferenceType): ReferenceType {
    return node;
  }
}

class Intrinsics {
  private _map: Map<string, (type: NativeType) => void> = new Map();
  void?: NativeType;
  int32?: NativeType;
  uint32?: NativeType;
  uint8?: NativeType;
  float?: NativeType;
  double?: NativeType;
  bool?: NativeType;

  isBitwiseEquivalent(left: number, right: number): boolean {
    let doubleArray = new Float64Array(1);
    let intArray = new Int32Array(doubleArray.buffer);
    doubleArray[0] = left;
    let leftInts = Int32Array.from(intArray);
    doubleArray[0] = right;
    for (let i = 0; i < WSL_2; ++i) {
      if (leftInts[i] !== intArray[i]) {
        return false;
      }
    }
    return true;
  }

  constructor(nameContext: NameContext) {
    // NOTE: Intrinsic resolution happens before type name resolution, so the strings we use here
    // to catch the intrinsics must be based on the type names that StandardLibraryPrologue.js uses.
    // For example, if a native function is declared using 'int' rather than 'int32', then we must
    // use 'int' here, since we don't yet know that they are the same type.

    this._map.set('native typedef void<>', (type: Node) => {
      let tmpType = type as NativeType;
      this.void = tmpType;
      tmpType.size = 0;
      tmpType.populateDefaultValue = (): void => {};
    });

    this._map.set('native typedef int32<>', (type: Node) => {
      let tmpType = type as NativeType;
      this.int32 = tmpType;
      tmpType.isPrimitive = true;
      tmpType.isInt = true;
      tmpType.isNumber = true;
      tmpType.isSigned = true;
      tmpType.canRepresent = (value: number): boolean => {
        return this.isBitwiseEquivalent(value | 0, value);
      };
      tmpType.size = 1;
      tmpType.defaultValue = 0;
      tmpType.createLiteral = (origin: LexerToken, value: number): GenericLiteral => {
        return IntLiteral.withType(origin, value | 0, tmpType);
      };
      tmpType.successorValue = (value: number): number => {
        return (value + 1) | 0;
      };
      tmpType.valuesEqual = (a: number, b: number): boolean => {
        return a === b;
      };
      tmpType.populateDefaultValue = (buffer: EBuffer, offset: number): void => {
        return buffer.eBufferSet(offset, 0);
      };
      tmpType.formatValueFromIntLiteral = (value: number): number => {
        return value | 0;
      };
      tmpType.formatValueFromUintLiteral = (value: number): number => {
        return value | 0;
      };
      tmpType.allValues = (): Array<ResultMemberInfo> => {
        let allValue: Array<ResultMemberInfo> = [];
        for (let i = 0; i <= WSL_0XFFFFFFFF; ++i) {
          let value = i | 0;
          allValue.push(new ResultMemberInfo(value, `${value}`));
        }
        return allValue;
      };
    });

    this._map.set('native typedef uint32<>', (type: Node) => {
      let tmpType = type as NativeType;
      this.uint32 = tmpType;
      tmpType.isPrimitive = true;
      tmpType.isInt = true;
      tmpType.isNumber = true;
      tmpType.isSigned = false;
      tmpType.canRepresent = (value: number): boolean => {
        return this.isBitwiseEquivalent(value >>> 0, value);
      };
      tmpType.size = 1;
      tmpType.defaultValue = 0;
      tmpType.createLiteral = (origin: LexerToken, value: number): GenericLiteral => {
        return IntLiteral.withType(origin, value >>> 0, tmpType);
      };
      tmpType.successorValue = (value: number): number => {
        return (value + 1) >>> 0;
      };
      tmpType.valuesEqual = (a: number, b: number): boolean => {
        return a === b;
      };
      tmpType.populateDefaultValue = (buffer: EBuffer, offset: number): void => {
        return buffer.eBufferSet(offset, 0);
      };
      tmpType.formatValueFromIntLiteral = (value: number): number => {
        return value >>> 0;
      };
      tmpType.formatValueFromUintLiteral = (value: number): number => {
        return value >>> 0;
      };
      tmpType.allValues = (): Array<ResultMemberInfo> => {
        let allValue: Array<ResultMemberInfo> = [];
        for (let i = 0; i <= WSL_0XFFFFFFFF; ++i) {
          allValue.push(new ResultMemberInfo(i, `${i}`));
        }
        return allValue;
      };
    });

    this._map.set('native typedef uint8<>', (type: Node) => {
      let tmpType = type as NativeType;
      this.uint8 = tmpType;
      tmpType.isInt = true;
      tmpType.isNumber = true;
      tmpType.isSigned = false;
      tmpType.canRepresent = (value: number): boolean => {
        return this.isBitwiseEquivalent(value & WSL_0XFF, value);
      };
      tmpType.size = 1;
      tmpType.defaultValue = 0;
      tmpType.createLiteral = (origin: LexerToken, value: number): GenericLiteral => {
        return IntLiteral.withType(origin, value & WSL_0XFF, tmpType);
      };
      tmpType.successorValue = (value: number): number => {
        return (value + 1) & WSL_0XFF;
      };
      tmpType.valuesEqual = (a: number, b: number): boolean => {
        return a === b;
      };
      tmpType.populateDefaultValue = (buffer: EBuffer, offset: number): void => {
        return buffer.eBufferSet(offset, 0);
      };
      tmpType.formatValueFromIntLiteral = (value: number): number => {
        return value & WSL_0XFF;
      };
      tmpType.formatValueFromUintLiteral = (value: number): number => {
        return value & WSL_0XFF;
      };
      tmpType.allValues = (): Array<ResultMemberInfo> => {
        let allValue: Array<ResultMemberInfo> = [];
        for (let i = 0; i <= WSL_0XFF; ++i) {
          allValue.push(new ResultMemberInfo(i, `${i}`));
        }
        return allValue;
      };
    });
    this._map.set('native typedef float32<>', (type: Node) => {
      let tmpType = type as NativeType;
      this.float = tmpType;
      tmpType.isPrimitive = true;
      tmpType.size = 1;
      tmpType.isFloating = true;
      tmpType.isNumber = true;
      tmpType.canRepresent = (value: number): boolean => {
        return this.isBitwiseEquivalent(Math.fround(value), value);
      };
      tmpType.populateDefaultValue = (buffer: EBuffer, offset: number): void => {
        return buffer.eBufferSet(offset, 0);
      };
      tmpType.formatValueFromIntLiteral = (value: number): number => {
        return value;
      };
      tmpType.formatValueFromUintLiteral = (value: number): number => {
        return value;
      };
      tmpType.formatValueFromFloatLiteral = (value: number): number => {
        return Math.fround(value);
      };
      tmpType.formatValueFromDoubleLiteral = (value: number): number => {
        return Math.fround(value);
      };
    });

    this._map.set('native typedef float64<>', (type: Node) => {
      let tmpType = type as NativeType;
      this.double = tmpType;
      tmpType.isPrimitive = true;
      tmpType.size = 1;
      tmpType.isFloating = true;
      tmpType.isNumber = true;
      tmpType.canRepresent = (value: number): boolean => {
        return true;
      };
      tmpType.populateDefaultValue = (buffer: EBuffer, offset: number): void => {
        buffer.eBufferSet(offset, 0);
      };
      tmpType.formatValueFromIntLiteral = (value: number): number => {
        return value;
      };
      tmpType.formatValueFromUintLiteral = (value: number): number => {
        return value;
      };
      tmpType.formatValueFromFloatLiteral = (value: number): number => {
        return value;
      };
      tmpType.formatValueFromDoubleLiteral = (value: number): number => {
        return value;
      };
    });

    this._map.set('native typedef bool<>', (type: Node) => {
      let tmpType = type as NativeType;
      this.bool = tmpType;
      tmpType.isPrimitive = true;
      tmpType.size = 1;
      tmpType.populateDefaultValue = (buffer: EBuffer, offset: number): void => {
        buffer.eBufferSet(offset, false);
      };
    });

    this._map.set('native operator<> int32(uint32)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) | 0);
      };
    });

    this._map.set('native operator<> int32(uint8)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) | 0);
      };
    });

    this._map.set('native operator<> int32(float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) | 0);
      };
    });

    this._map.set('native operator<> int32(double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) | 0);
      };
    });

    this._map.set('native operator<> uint32(int32)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) | 0);
      };
    });

    this._map.set('native operator<> uint32(uint8)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) | 0);
      };
    });
    this._map.set('native operator<> uint32(float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) | 0);
      };
    });

    this._map.set('native operator<> uint32(double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) | 0);
      };
    });

    this._map.set('native operator<> uint8(int32)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) & WSL_0XFF);
      };
    });

    this._map.set('native operator<> uint8(uint32)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) & WSL_0XFF);
      };
    });

    this._map.set('native operator<> uint8(float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) & WSL_0XFF);
      };
    });

    this._map.set('native operator<> uint8(double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) & WSL_0XFF);
      };
    });

    this._map.set('native operator<> float(double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(Math.fround(value[0].loadValue() as number));
      };
    });

    this._map.set('native operator<> float(int32)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(Math.fround(value[0].loadValue() as number));
      };
    });

    this._map.set('native operator<> float(uint32)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(Math.fround(value[0].loadValue() as number));
      };
    });

    this._map.set('native operator<> float(uint8)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(Math.fround(value[0].loadValue() as number));
      };
    });

    this._map.set('native operator<> double(float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue());
      };
    });

    this._map.set('native operator<> double(int32)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue());
      };
    });

    this._map.set('native operator<> double(uint32)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue());
      };
    });

    this._map.set('native operator<> double(uint8)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue());
      };
    });

    this._map.set('native int operator+<>(int,int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(((value[0].loadValue() as number) + (value[1].loadValue() as number)) | 0);
      };
    });

    this._map.set('native uint operator+<>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(((value[0].loadValue() as number) + (value[1].loadValue() as number)) >>> 0);
      };
    });

    this._map.set('native float operator+<>(float,float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(Math.fround((value[0].loadValue() as number) + (value[1].loadValue() as number)));
      };
    });

    this._map.set('native double operator+<>(double,double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) + (value[1].loadValue() as number));
      };
    });

    this._map.set('native int operator-<>(int,int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(((value[0].loadValue() as number) - (value[1].loadValue() as number)) | 0);
      };
    });

    this._map.set('native uint operator-<>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(((value[0].loadValue() as number) - (value[1].loadValue() as number)) >>> 0);
      };
    });

    this._map.set('native float operator-<>(float,float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(Math.fround((value[0].loadValue() as number) - (value[1].loadValue() as number)));
      };
    });

    this._map.set('native double operator-<>(double,double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) - (value[1].loadValue() as number));
      };
    });

    this._map.set('native int operator*<>(int,int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(((value[0].loadValue() as number) * (value[1].loadValue() as number)) | 0);
      };
    });

    this._map.set('native uint operator*<>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(((value[0].loadValue() as number) * (value[1].loadValue() as number)) >>> 0);
      };
    });

    this._map.set('native float operator*<>(float,float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(Math.fround((value[0].loadValue() as number) * (value[1].loadValue() as number)));
      };
    });

    this._map.set('native double operator*<>(double,double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) * (value[1].loadValue() as number));
      };
    });

    this._map.set('native int operator/<>(int,int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(((value[0].loadValue() as number) / (value[1].loadValue() as number)) | 0);
      };
    });

    this._map.set('native uint operator/<>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(((value[0].loadValue() as number) / (value[1].loadValue() as number)) >>> 0);
      };
    });

    this._map.set('native int operator&<>(int,int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) & (value[1].loadValue() as number));
      };
    });

    this._map.set('native uint operator&<>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(((value[0].loadValue() as number) & (value[1].loadValue() as number)) >>> 0);
      };
    });

    this._map.set('native int operator|<>(int,int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) | (value[1].loadValue() as number));
      };
    });

    this._map.set('native uint operator|<>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(((value[0].loadValue() as number) | (value[1].loadValue() as number)) >>> 0);
      };
    });

    this._map.set('native int operator^<>(int,int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) ^ (value[1].loadValue() as number));
      };
    });

    this._map.set('native uint operator^<>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(((value[0].loadValue() as number) ^ (value[1].loadValue() as number)) >>> 0);
      };
    });

    this._map.set('native int operator<<<>(int,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) << (value[1].loadValue() as number));
      };
    });

    this._map.set('native uint operator<<<>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(((value[0].loadValue() as number) << (value[1].loadValue() as number)) >>> 0);
      };
    });

    this._map.set('native int operator>><>(int,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) >> (value[1].loadValue() as number));
      };
    });

    this._map.set('native uint operator>><>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) >>> (value[1].loadValue() as number));
      };
    });

    this._map.set('native int operator~<>(int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(~(value[0].loadValue() as number));
      };
    });

    this._map.set('native uint operator~<>(uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(~(value[0].loadValue() as number) >>> 0);
      };
    });

    this._map.set('native float operator/<>(float,float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(Math.fround((value[0].loadValue() as number) / (value[1].loadValue() as number)));
      };
    });

    this._map.set('native double operator/<>(double,double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box((value[0].loadValue() as number) / (value[1].loadValue() as number));
      };
    });

    this._map.set('native bool operator==<>(int,int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue() === value[1].loadValue());
      };
    });

    this._map.set('native bool operator==<>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue() === value[1].loadValue());
      };
    });

    this._map.set('native bool operator==<>(bool,bool)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue() === value[1].loadValue());
      };
    });

    this._map.set('native bool operator==<>(float,float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue() === value[1].loadValue());
      };
    });

    this._map.set('native bool operator==<>(double,double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue() === value[1].loadValue());
      };
    });

    this._map.set('native bool operator<<>(int,int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! < value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator<<>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! < value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator<<>(float,float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! < value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator<<>(double,double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! < value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator<=<>(int,int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! <= value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator<=<>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! <= value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator<=<>(float,float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! <= value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator<=<>(double,double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! <= value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator><>(int,int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! > value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator><>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! > value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator><>(float,float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! > value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator><>(double,double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! > value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator>=<>(int,int)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! >= value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator>=<>(uint,uint)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! >= value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator>=<>(float,float)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! >= value[1].loadValue()!);
      };
    });

    this._map.set('native bool operator>=<>(double,double)', (func1: Node) => {
      (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
        return EPtr.box(value[0].loadValue()! >= value[1].loadValue()!);
      };
    });

    for (let addressSpace of addressSpaces) {
      this._map.set(`native T* ${addressSpace} operator&[]<T>(T[] ${addressSpace},uint)`, (func1: Node) => {
        (func1 as NativeFunc).implementation = (value: Array<EPtr>, node: CallExpression | null): EPtr | null => {
          let ref: EArrayRef | null = value[0].loadValue() as EArrayRef;
          if (ref === null) {
            throw new WTrapError(node!.origin!.originString!, 'Null dereference');
          }
          let index = value[1].loadValue() as number;
          if (index > ref!.length) {
            throw new WTrapError(node!.origin!.originString!, `Array index ${value[1].toString()} is out of bounds of ${value[0].toString()}`);
          }
          return EPtr.box(ref.ptr.plus(index * (node!.instantiatedActualTypeArguments![0] as HmType).size!));
        };
      });

      this._map.set(`native uint operator.length<T>(T[] ${addressSpace})`, (func1: Node) => {
        (func1 as NativeFunc).implementation = (value: Array<EPtr>): EPtr => {
          let eptr: EArrayRef | null = value[0].loadValue() as EArrayRef;
          if (eptr === null) {
            return EPtr.box(0);
          }
          return EPtr.box(eptr!.length);
        };
      });
    }
  }

  add(thing: Node): void {
    if (thing instanceof NativeType) {
      let intrinsic = this._map.get(thing.toString()) as (type: Node) => void;
      if (intrinsic === null) {
        throw new WTypeError(thing.origin!.originString, `Unrecognized intrinsic: ${thing.toString()}`);
      }
      intrinsic(thing);
    } else {
      let intrinsic = this._map.get(thing.toString()) as (type: Node) => void;
      if (intrinsic === null) {
        throw new WTypeError(thing.origin!.originString, `Unrecognized intrinsic: ${thing.toString()}`);
      }
      intrinsic(thing as NativeFunc);
    }
  }
}
class LateChecker extends Visitor {
  visitReferenceType(node: Node): void {
    let nodes = node as ReferenceType;
    if (nodes.addressSpace === 'thread') {
      return;
    }
    if (!nodes.elementType!.isPrimitive) {
      throw new WTypeError(nodes.origin!.originString, `Illegal pointer to non-primitive type: ${nodes.toString()}`);
    }
  }

  checkShaderType(node: FuncDef): void {
    // FIXME: Tighten these checks. For now, we should only accept int32, uint32, float32, and float64.
    let assertPrimitive = (type: TypeRef): void => {
      if (!type.isPrimitive) {
        throw new WTypeError(node.origin!.originString + `Shader signature cannot include non-primitive type: ${type.toString()}`);
      }
    };
    assertPrimitive(node.returnType as TypeRef);
    if (!(node.returnType.unifyNode instanceof StructType)) {
      throw new WTypeError(node.origin!.originString, `Vertex shader ${node.name} must return a struct.`);
    }
    switch (node.shaderType) {
      case 'vertex':
        for (let parameter of node.parameters) {
          if (parameter.hmType!.unifyNode instanceof StructType) {
            assertPrimitive(parameter.hmType as TypeRef);
          } else if (!(parameter.hmType!.unifyNode as PtrType).isArrayRef) {
            throw new WTypeError(node.origin!.originString, `${node.name} accepts a parameter ${parameter.name} which isn't a struct and isn't an ArrayRef.`);
          }
        }
        break;
      case 'fragment':
        for (let parameter of node.parameters) {
          if (parameter.name === 'stageIn') {
            if (!(parameter.hmType!.unifyNode instanceof StructType)) {
              throw new WTypeError(node.origin!.originString, `Fragment entry points' stageIn parameter (of ${node.name}) must be a struct type.`);
            }
            assertPrimitive(parameter.hmType as TypeRef);
          } else {
            if (!(parameter.hmType!.unifyNode as HmType).isArrayRef) {
              throw new WTypeError(node.origin!.originString, `Fragment entry point's ${parameter.name} parameter is not an array reference.`);
            }
          }
        }
        break;
      default:
        throw new Error(`Bad shader type: ${node.shaderType}`);
    }
  }

  visitFuncDef(node: Node): void {
    if ((node as FuncDef).shaderType !== null) {
      this.checkShaderType(node as FuncDef);
    }
    super.visitFuncDef(node as FuncDef);
  }
}
let regExpLastMatch = '';
function setLastMatch(regex: RegExp, text: string): boolean {
  let result = regex.exec(text);
  if (result !== null) {
    regExpLastMatch = result![0];
  }
  return result != null;
}
class Lexer {
  private _origin: string;
  private _originKind: string;
  private _lineNumberOffset: number;
  private _text: string;
  private _index: number;
  _stack: (LexerToken | null)[];

  constructor(origin: string, originKind: string, lineNumberOffset: number, text: string) {
    if (!isOriginKind(originKind)) {
      throw new Error(`Bad origin kind: ${originKind}`);
    }
    this._origin = origin;
    this._originKind = originKind;
    this._lineNumberOffset = lineNumberOffset;
    this._text = text;
    this._index = 0;
    this._stack = Array();
  }

  get lineNumber(): number {
    return this.lineNumberForIndex(this._index);
  }

  get origin(): string {
    return this._origin;
  }

  get originString(): string {
    return `${this._origin}:${this.lineNumber + 1}`;
  }

  get originKind(): string {
    return this._originKind;
  }

  lineNumberForIndex(index: number): number {
    let matchesAll = this._text.substring(0, index).matchAll(new RegExp('\\n', 'g'));
    let matches = Array.from(matchesAll);
    return (matches ? matches.length : 0) + this._lineNumberOffset;
  }

  get state(): StateResult {
    return new StateResult(this._index, this._stack.concat());
  }

  set state(newValue: StateResult) {
    this._index = newValue.index;
    this._stack = newValue.stack;
  }

  static _textIsIdentifierImpl(text: string): boolean {
    let regex = new RegExp('^[^\\d\\W]\\w*');
    return setLastMatch(regex, text);
  }

  static textIsIdentifier(text: string): boolean {
    let regex = new RegExp('^[^\\d\\W]\\w*');
    let rs = text.split(regex).join('');
    return Lexer._textIsIdentifierImpl(text) && rs.length === 0;
  }

  next(): LexerToken | null {
    if (this._stack.length > 0) {
      return this._stack.pop() ?? null;
    }
    if (this._index >= this._text.length) {
      return null;
    }
    let result = (kind: string): LexerToken => {
      let text: string = regExpLastMatch;
      let token = new LexerToken(this, this._index, kind, text);
      this._index += text.length;
      return token;
    };

    let relevantText: string;

    while (true) {
      relevantText = this._text.substring(this._index);
      let regExp = new RegExp('^\\s+');
      if (setLastMatch(regExp, relevantText)) {
        this._index += regExpLastMatch.length;
        continue;
      }
      let regExp1 = new RegExp('^\\/\\*');
      if (setLastMatch(regExp1, relevantText)) {
        let endIndex = relevantText.search(new RegExp('\\*\\/'));
        if (endIndex < 0) {
          this.fail('Unterminated comment');
        }
        this._index += endIndex;
        continue;
      }
      let regExp2 = new RegExp('^\\/\\/.*');
      if (setLastMatch(regExp2, relevantText)) {
        this._index += regExpLastMatch.length;
        continue;
      }
      break;
    }
    if (this._index >= this._text.length) {
      return null;
    }

    // FIXME: Make this handle exp-form literals like 1e1.
    let regExp3 = new RegExp('^(([0-9]*\\.[0-9]+[fd]?)|([0-9]+\\.[0-9]*[fd]?))');
    if (setLastMatch(regExp3, relevantText)) {
      return result('floatLiteral');
    }

    // FIXME: Make this do Unicode.
    let unicodeRegExp = new RegExp(
      '^(struct|protocol|typedef|if|else|enum|continue|break|switch|case|default|for|while|do|return|constant|device|threadgroup|thread|operator|null|true|false)$'
    );
    if (Lexer._textIsIdentifierImpl(relevantText)) {
      if (setLastMatch(unicodeRegExp, regExpLastMatch)) {
        return result('keyword');
      }

      if (this._originKind === 'native' && setLastMatch(new RegExp('^(native|restricted)$'), regExpLastMatch)) {
        return result('keyword');
      }

      return result('identifier');
    }

    let uinHexRegExp = new RegExp('^0x[0-9a-fA-F]+u');
    if (setLastMatch(uinHexRegExp, relevantText)) {
      return result('uintHexLiteral');
    }
    let intHexRegExp = new RegExp('^0x[0-9a-fA-F]+');
    if (setLastMatch(intHexRegExp, relevantText)) {
      return result('intHexLiteral');
    }
    let uinRegExp = new RegExp('^[0-9]+u');
    if (setLastMatch(uinRegExp, relevantText)) {
      return result('uintLiteral');
    }
    let inRegExp = new RegExp('^[0-9]+');
    if (setLastMatch(inRegExp, relevantText)) {
      return result('intLiteral');
    }
    let punctuaRegExp = new RegExp('^<<|>>|->|>=|<=|==|!=|\\+=|-=|\\*=|\\/=|%=|\\^=|\\|=|&=|\\+\\+|--|&&|\\|\\||([{}()\\[\\]?:=+*\\/,.%!~^&|<>@;-])');
    if (setLastMatch(punctuaRegExp, relevantText)) {
      return result('punctuation');
    }

    let remaining = relevantText.substring(0, WSL_20).split(new RegExp('\\s'))[0];
    if (remaining.length === 0) {
      remaining = relevantText.substring(0, WSL_20);
    }
    this.fail(`Unrecognized token beginning with: ${remaining}`);
    return null;
  }

  push(token: LexerToken | null): void {
    this._stack.push(token);
  }

  peek(): LexerToken | null {
    let result = this.next();
    this.push(result);
    return result;
  }

  fail(error: string): void {
    throw new WSyntaxError(this.originString, error);
  }

  backtrackingScope(callback: () => Node | null): Node | null {
    let state = this.state;
    try {
      return callback();
    } catch (e) {
      if (e instanceof Error) {
        this.state = state;
        return null;
      }
      throw e as Error;
    }
  }

  testScope(callback: () => void): boolean {
    let state = this.state;
    try {
      callback();
      return true;
    } catch (e) {
      if (e instanceof Error) {
        return false;
      }
      throw e as Error;
    } finally {
      this.state = state;
    }
  }
}

class LexerToken {
  private _lexer: Lexer;
  _index: number | null;
  _kind: string;
  _text: string;
  constructor(lexer: Lexer, index: number, kind: string, text: string) {
    this._lexer = lexer;
    this._index = index;
    this._kind = kind;
    this._text = text;
  }

  get lexer(): Lexer {
    return this._lexer;
  }

  get kind(): string {
    return this._kind;
  }

  get text(): string {
    return this._text;
  }

  get origin(): string {
    return this.lexer.origin;
  }

  get originKind(): string {
    return this.lexer.originKind;
  }

  get index(): number | null {
    return this._index;
  }

  get lineNumber(): number {
    return this._lexer.lineNumberForIndex(this._index!);
  }

  get originString(): string {
    return `${this.origin}:${this.lineNumber + 1}`;
  }

  toString(): string {
    return `LexerToken(${this.kind}, ${this.text}, ${this.lineNumber})`;
  }
}

class LiteralTypeChecker extends Visitor {
  visitNullType(node: Node): void {
    if (node.hmType === null) {
      throw new Error(`Null at ${node.origin!.originString} does not have type`);
    }
  }

  visitGenericLiteralType(node: Node): void {
    if (node.hmType === null) {
      throw new Error(`${node.toString()} at ${node.origin!.originString} does not have type`);
    }
  }
}

class LogicalExpression extends Expression {
  private _text: string = '';
  private _left: HmValue;
  private _right: HmValue;

  constructor(origin: LexerToken, text: string, left: HmValue, right: HmValue) {
    super(origin);
    this._text = text;
    this._left = left;
    this._right = right;
  }

  get text(): string {
    return this._text;
  }

  get left(): HmValue {
    return this._left;
  }

  get right(): HmValue {
    return this._right;
  }

  toString(): string {
    return `(${this.left.toString()} ${this.text} ${this.right.toString()})`;
  }
}

class LogicalNot extends Expression {
  private _operand: HmValue;
  constructor(origin: LexerToken, operand: HmValue) {
    super(origin);
    this._operand = operand;
  }

  get operand(): HmValue {
    return this._operand;
  }

  toString(): string {
    return `!(${this.operand.toString()})`;
  }
}

class LoopChecker extends Visitor {
  private _loopDepth: number = 0;
  private _switchDepth: number = 0;
  constructor() {
    super();
    this._loopDepth = 0;
    this._switchDepth = 0;
  }

  visitFuncDef(node: Node): void {
    if (this._loopDepth !== 0) {
      throw new WTypeError((node as FuncDef).origin!.originString, 'LoopChecker does not understand nested functions.');
    }
    super.visitFuncDef(node as FuncDef);
  }

  visitWhileLoop(node: Node): void {
    let nodes = node as WhileLoop;
    nodes.conditional.visit(this);
    this._loopDepth += 1;
    nodes.body.visit(this);
    if (this._loopDepth === 0) {
      throw new WTypeError(nodes.origin!.originString, 'The number of nested loops is negative!');
    }
    this._loopDepth -= 1;
  }

  visitDoWhileLoop(node: Node): void {
    let nodes = node as DoWhileLoop;
    this._loopDepth += 1;
    nodes.body.visit(this);
    if (this._loopDepth === 0) {
      throw new WTypeError(nodes.origin!.originString, 'The number of nested loops is negative!');
    }
    this._loopDepth -= 1;
    nodes.conditional.visit(this);
  }

  visitForLoop(node: Node): void {
    let nodes = node as ForLoop;
    if (nodes.initialization !== null) {
      nodes.initialization!.visit(this);
    }

    if (nodes.condition !== null) {
      nodes.condition!.visit(this);
    }

    if (nodes.increment !== null) {
      nodes.increment!.visit(this);
    }
    this._loopDepth += 1;
    nodes.body.visit(this);
    if (this._loopDepth === 0) {
      throw new WTypeError(nodes.origin!.originString, 'The number of nested loops is negative!');
    }
    this._loopDepth -= 1;
  }

  visitSwitchStatement(node: Node): void {
    let nodes = node as SwitchStatement;
    nodes.value.visit(this);
    this._switchDepth += 1;
    for (let switchCase of nodes.switchCases) {
      switchCase.visit(this);
    }
    this._switchDepth -= 1;
  }

  visitBreak(node: Node): void {
    let nodes = node as Break;
    if (this._loopDepth === 0 && this._switchDepth === 0) {
      throw new WTypeError(nodes.origin!.originString, 'Break statement without enclosing loop or switch!');
    }
    super.visitBreak(nodes);
  }

  visitContinue(node: Node): void {
    let nodes = node as Continue;
    if (this._loopDepth === 0) {
      throw new WTypeError(nodes.origin!.originString, 'Continue statement without enclosing loop!');
    }
    super.visitContinue(nodes);
  }
}

class MakeArrayRefExpression extends Expression {
  numElements: Expression | null = null;

  constructor(origin: LexerToken, lValue: HmValue) {
    super(origin);
    this.lValue = lValue;
  }
  toString(): string {
    if (this.isBecome) {
      return `(${this.target.toString()})`;
    } else {
      let string = this.numElements != null ? `<<${this.numElements.toString()}>>` : '';
      return `@${string}(${this.lValue!.toString()})`;
    }
  }
}

class MakePtrExpression extends Expression {
  constructor(origin: LexerToken, lValue: HmValue) {
    super(origin);
    this.lValue = lValue;
  }

  toString(): string {
    return `&(${this.lValue!.toString()})`;
  }
}
class NameContext {
  private _map: Map<string, Node | [] | KindArray>;
  private _set: Set<Node>;
  private _currentStatement: Node | null;
  private _delegate: NameContext | null;
  private _intrinsics: Intrinsics | null;
  private _program: Program | null;

  constructor(delegate: null | NameContext = null) {
    this._map = new Map<string, Node | [] | KindArray>();
    this._set = new Set<Node>();
    this._currentStatement = null;
    this._delegate = delegate;
    this._intrinsics = null;
    this._program = null;
  }

  add(thing: Node): void {
    if (thing.name === '') {
      return;
    }
    if (thing.origin === null) {
      throw new Error(`Thing does not have origin: ${thing}`);
    }
    if (thing.isNative && ((thing as NativeFunc).implementation ?? null) === null) {
      if (this._intrinsics === null) {
        throw new Error('Native function in a scope that does not recognize intrinsics');
      }
      this._intrinsics!.add(thing);
    }
    if (typeof thing.kind === 'string' && (thing.kind as string) === 'Func') {
      this._set.add(thing);
      let array = this._map.get(thing.name) as KindArray | null;
      if ((array ?? null) === null) {
        array = new KindArray();
        array.kind = 'Func';
        this._map.set(thing.name, array);
      }
      if (array!.kind !== 'Func') {
        throw new WTypeError(thing.origin.originString, `Cannot reuse type name for function: ${thing.name}`);
      }
      array!.array.push(thing as Func);
      this._map.set(thing.name, array!);
      return;
    }
    if (this._map.get(thing.name) != null) {
      throw new WTypeError(thing.origin.originString, `Duplicate name: ${thing.name}`);
    }
    this._set.add(thing);
    this._map.set(thing.name, thing);
  }

  get(kind: string | [] | KindArray | HmSymbol, name: string): Node | [] | KindArray | null {
    let result: Node | [] | KindArray | null = (this._map.get(name) as Node | [] | KindArray | null) ?? null;
    if (result === null && this._delegate !== null) {
      let temp: Node | [] | KindArray | null = this._delegate.get(kind, name);
      return temp;
    }
    let isKind = false;
    if (result != null && typeof kind === 'string') {
      if (result instanceof KindArray) {
        isKind = (result as KindArray).kind !== (kind as string);
      } else {
        isKind = ((result as Node).kind as string) !== (kind as string);
      }
    }
    if (result !== null && !isWildcardKind(kind) && isKind) {
      return null;
    }
    return result;
  }
  underlyingThings(kind: HmSymbol, name: string): Node[] {
    let things = this.get(kind, name) as Node | [] | null;
    return NameContext.underlyingThings(things);
  }
  static underlyingThings(thing: Node | [] | null): Node[] {
    if ((thing ?? null) === null) {
      return [];
    }
    if (thing! instanceof KindArray && (thing! as KindArray).kind === 'Func') {
      if (!(thing instanceof KindArray)) {
        throw new Error(`Func thing is not array: ${thing}`);
      }
      return (thing as KindArray).array;
    }
    return [thing as Node];
  }

  resolveFuncOverload(
    name: string,
    typeArguments: Node[],
    argumentTypes: HmType[],
    returnType: TypeRef | null,
    allowEntryPoint: boolean = false
  ): ResolveOverloadResult {
    let functions = this.get('Func', name);
    if (functions === null) {
      return new ResolveOverloadResult(null, [], null, null, null, null);
    }
    let newFunc: Func[] = [];
    if (functions instanceof KindArray) {
      newFunc = (functions as KindArray).array;
    } else {
      newFunc = functions as Func[];
    }
    return resolveOverloadImpl(newFunc, typeArguments, argumentTypes, returnType!, allowEntryPoint);
  }

  get currentStatement(): Node | null {
    {
      if (this._currentStatement !== null) {
        return this._currentStatement;
      }
      if (this._delegate !== null) {
        return this._delegate!.currentStatement;
      }
      return null;
    }
  }

  doStatement(statement: Node, callback: () => void): void {
    this._currentStatement = statement;
    callback();
    this._currentStatement = null;
  }

  recognizeIntrinsics(): void {
    this._intrinsics = new Intrinsics(this);
  }

  get intrinsics(): Intrinsics | null {
    {
      if (this._intrinsics !== null) {
        return this._intrinsics;
      }
      if (this._delegate !== null) {
        return this._delegate!.intrinsics;
      }
      return null;
    }
  }

  set program(newValue: Program | null) {
    this._program = newValue;
  }
  get program(): Program | null {
    {
      if (this._program !== null) {
        return this._program!;
      }
      if (this._delegate !== null) {
        return this._delegate!.program;
      }
      return null;
    }
  }
}
// NameFinder extends Visitor
class NameFinder extends Visitor {
  set: Set<string>;
  workList: string[];

  constructor() {
    super();
    this.set = new Set();
    this.workList = [];
  }

  add(name: string): void {
    if (this.set.has(name)) {
      return;
    }
    this.set.add(name);
    this.workList.push(name);
  }

  visitProtocolRef(node: Node): void {
    this.add(node.name);
    super.visitProtocolRef(node);
  }

  visitTypeRef(node: Node): void {
    this.add(node.name);
    super.visitTypeRef(node);
  }

  visitVariableRef(node: Node): void {
    this.add(node.name);
    super.visitVariableRef(node);
  }

  visitTypeOrVariableRef(node: Node): void {
    this.add(node.name);
  }

  handlePropertyAccess(node: PropertyAccessExpression): void {
    this.add(node.getFuncName);
    this.add(node.setFuncName);
    this.add(node.andFuncName);
  }

  visitDotExpression(node: Node): void {
    this.handlePropertyAccess(node as PropertyAccessExpression);
    super.visitDotExpression(node);
  }

  visitIndexExpression(node: Node): void {
    this.handlePropertyAccess(node as PropertyAccessExpression);
    super.visitIndexExpression(node as IndexExpression);
  }

  visitCallExpression(node: Node): void {
    this.add(node.name);
    super.visitCallExpression(node as CallExpression);
  }

  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    let tmp: Map<string, (node: Base) => void | Base | string | null> = super.mapReturn();
    tmp.set('visitTypeOrVariableRef', this.visitTypeOrVariableRef as (node: Base) => void);
    return tmp;
  }
}
///class NameResolver extends Visitor
class NameResolver extends Visitor {
  private _nameContext: NameContext;

  constructor(nameContext: NameContext) {
    super();
    this._nameContext = nameContext;
  }

  doStatement(statement: Node): void {
    this._nameContext.doStatement(statement, () => {
      return statement.visit(this);
    });
  }

  visitTypeParametersAndBuildNameContext(node: Func): NameContext {
    let nameContext = new NameContext(this._nameContext);
    for (let typeParameter of node.typeParameters) {
      nameContext.add(typeParameter);
      typeParameter.visit(this);
    }
    return nameContext;
  }
  visitFunc(node: Node): void {
    let newNode = node as Func;
    let checker = new NameResolver(this.visitTypeParametersAndBuildNameContext(newNode));
    newNode.returnType.visit(checker);
    for (let parameter of newNode.parameters) {
      parameter.visit(checker);
    }
  }

  visitFuncDef(node: Node): void {
    let newNode = node as FuncDef;
    let contextWithTypeParameters = this.visitTypeParametersAndBuildNameContext(newNode);
    let checkerWithTypeParameters = new NameResolver(contextWithTypeParameters);
    newNode.returnType.visit(checkerWithTypeParameters);
    let contextWithParameters = new NameContext(contextWithTypeParameters);
    for (let parameter of newNode.parameters) {
      parameter.visit(checkerWithTypeParameters);
      contextWithParameters.add(parameter as Node);
    }
    let checkerWithParameters = new NameResolver(contextWithParameters);
    newNode.body.visit(checkerWithParameters);
  }

  visitBlock(node: Node): void {
    let checker = new NameResolver(new NameContext(this._nameContext));
    for (let statement of (node as Block).statements) {
      statement.visit(checker);
    }
  }

  visitIfStatement(node: Node): void {
    let newNode = node as IfStatement;
    newNode.conditional.visit(this);
    // The bodies might not be Blocks, so we need to explicitly give them a new context.
    newNode.body.visit(new NameResolver(new NameContext(this._nameContext)));
    if (newNode.elseBody != null) {
      newNode.elseBody!.visit(new NameResolver(new NameContext(this._nameContext)));
    }
  }

  visitWhileLoop(node: Node): void {
    let newNode = node as WhileLoop;
    newNode.conditional.visit(this);
    // The bodies might not be Blocks, so we need to explicitly give them a new context.
    newNode.body.visit(new NameResolver(new NameContext(this._nameContext)));
  }

  visitDoWhileLoop(node: Node): void {
    let newNode = node as DoWhileLoop;
    // The bodies might not be Blocks, so we need to explicitly give them a new context.
    newNode.body.visit(new NameResolver(new NameContext(this._nameContext)));
    newNode.conditional.visit(this);
  }

  visitForLoop(node: Node): void {
    let newNode = node as ForLoop;
    let newResolver = new NameResolver(new NameContext(this._nameContext));
    if (newNode.initialization !== null) {
      newNode.initialization!.visit(newResolver);
    }
    if (newNode.condition !== null) {
      newNode.condition!.visit(newResolver);
    }
    if (newNode.increment !== null) {
      newNode.increment!.visit(newResolver);
    }
    newNode.body.visit(newResolver);
  }

  visitProtocolDecl(node: Node): void {
    let newNode = node as ProtocolDecl;
    for (let parent of newNode.extends) {
      parent.visit(this);
    }
    let nameContext = new NameContext(this._nameContext);
    nameContext.add(newNode.typeVariable);
    let checker = new NameResolver(nameContext);
    for (let signature of newNode.signatures) {
      signature.visit(checker);
    }
  }

  visitProtocolRef(node: Node): void {
    let newNode = node as ProtocolRef;
    let result = this._nameContext.get('Protocol', newNode.name);
    if (result === null) {
      throw new WTypeError(newNode.origin!.originString, `Could not find protocol named ${newNode.name}`);
    }
    newNode.protocolDecl = result as ProtocolDecl | null;
  }

  visitProtocolFuncDecl(node: Node): void {
    let newNode = node as ProtocolFuncDecl;
    this.visitFunc(newNode);
    let funcs = this._nameContext.get('Func', newNode.name);
    if (funcs == null) {
      throw new WTypeError(newNode.origin!.originString, `Cannot find any functions named ${newNode.name}`);
    }
    newNode.possibleOverloads = (funcs as KindArray).array;
  }

  visitTypeDef(node: Node): void {
    let nameContext = new NameContext(this._nameContext);
    for (let typeParameter of (node as TypeDef).typeParameters) {
      typeParameter.visit(this);
      nameContext.add(typeParameter);
    }
    let checker = new NameResolver(nameContext);
    node.hmType!.visit(checker);
  }

  visitStructType(node: Node): void {
    let newNode = node as StructType;
    let nameContext = new NameContext(this._nameContext);
    for (let typeParameter of newNode.typeParameters) {
      typeParameter.visit(this);
      nameContext.add(typeParameter);
    }
    let checker = new NameResolver(nameContext);
    for (let field of newNode.fields) {
      field.visit(checker);
    }
  }

  resolveTypeArguments(typeArguments: Array<Node>): void {
    for (let i = 0; i < typeArguments.length; ++i) {
      let typeArgument = typeArguments[i];
      if (typeArgument instanceof TypeOrVariableRef) {
        let thing = this._nameContext.get(anything, typeArgument.name);
        if (thing === null) {
          throw new WTypeError(typeArgument.origin!.originString, `Could not find type or variable named ${typeArgument.name}`);
        }
        if (thing instanceof HmValue) {
          typeArguments[i] = new VariableRef(typeArgument.origin!, typeArgument.name);
        } else if (thing instanceof HmType) {
          typeArguments[i] = new TypeRef(typeArgument.origin!, typeArgument.name, []);
        } else {
          throw new WTypeError(typeArgument.origin!.originString, `Type argument resolved to wrong kind of thing: ${(thing as Node).kind ?? ''}`);
        }
      }
    }
  }

  visitTypeRef(node: Node): void {
    let newNode = node as TypeRef;
    this.resolveTypeArguments(newNode.typeArguments!);

    let hmType = newNode.hmType;
    if (hmType === null) {
      hmType = this._nameContext.get('Type', newNode.name!) as HmType | null;
      if (hmType === null) {
        throw new WTypeError(newNode.origin!.originString, `Could not find type named ${newNode.name}`);
      }
      newNode.hmType = hmType;
    }
    if (hmType!.typeParameters.length !== newNode.typeArguments!.length) {
      throw new WTypeError(
        newNode.origin!.originString,
        `Wrong number of type arguments (passed ${newNode.typeArguments!.length}, expected ${hmType.typeParameters.length})`
      );
    }
    for (let i = 0; i < hmType.typeParameters.length; ++i) {
      let parameterIsType = hmType!.typeParameters[i] instanceof TypeVariable;
      let argumentIsType = newNode.typeArguments![i] instanceof HmType;
      newNode.typeArguments![i].visit(this);
      if (parameterIsType && !argumentIsType) {
        throw new WTypeError(newNode.origin!.originString, `Expected type, but got value at argument #${i}`);
      }
      if (!parameterIsType && argumentIsType) {
        throw new WTypeError(newNode.origin!.originString, `Expected value, but got type at argument #${i}`);
      }
    }
  }
  visitReferenceType(node: Node): void {
    let newNode = node as ReferenceType;
    let nameContext = new NameContext(this._nameContext);
    newNode.elementType!.visit(new NameResolver(nameContext));
  }

  visitVariableDecl(node: Node): void {
    let newNode = node as VariableDecl;
    this._nameContext.add(newNode);
    newNode.hmType!.visit(this);
    if (newNode.initializer !== null) {
      newNode.initializer!.visit(this);
    }
  }

  visitVariableRef(node: Node): void {
    let newNode = node as VariableRef;
    if (newNode.variable !== null) {
      return;
    }
    let result = this._nameContext.get('Value', newNode.name);
    if (result === null) {
      throw new WTypeError(newNode.origin!.originString, `Could not find variable named ${newNode.name}`);
    }
    newNode.variable = result as HmValue | null;
  }

  visitReturn(node: Node): void {
    let newNode = node as Return;
    newNode.func1 = this._nameContext.currentStatement as FuncDef | null;
    super.visitReturn(newNode);
  }

  handlePropertyAccess(node: PropertyAccessExpression): void {
    node.possibleGetOverloads = (this._nameContext.get('Func', node.getFuncName) as KindArray)?.array ?? [];
    node.possibleSetOverloads = (this._nameContext.get('Func', node.setFuncName) as KindArray)?.array ?? [];
    node.possibleAndOverloads = (this._nameContext.get('Func', node.andFuncName) as KindArray)?.array ?? [];

    if (node.possibleGetOverloads.length === 0 && node.possibleAndOverloads.length === 0) {
      throw new WTypeError(node.origin!.originString, `Cannot find either ${node.getFuncName} or ${node.andFuncName}`);
    }
  }

  visitDotExpression(node: Node): void {
    let newNode = node as DotExpression;
    // This could be a reference to an enum. Let's resolve that now.
    if (newNode.struct1 instanceof VariableRef) {
      let enumType = this._nameContext.get('Type', newNode.struct1!.name) as EnumType | null;
      if (enumType !== null && enumType instanceof EnumType) {
        let enumMember = enumType.memberByName(newNode.fieldName);
        if (enumMember === null) {
          throw new WTypeError(newNode.origin!.originString, `Enum ${enumType.name} does not have a member named ${newNode.fieldName}`);
        }
        newNode.become(new EnumLiteral(newNode.origin!, enumMember));
        return;
      }
    }

    this.handlePropertyAccess(newNode);
    super.visitDotExpression(newNode);
  }

  visitIndexExpression(node: Node): void {
    let newNode = node as IndexExpression;
    this.handlePropertyAccess(newNode);
    super.visitIndexExpression(newNode);
  }

  visitCallExpression(node: Node): void {
    let newNode = node as CallExpression;
    this.resolveTypeArguments(newNode.typeArguments);

    let funcs = this._nameContext.get('Func', newNode.name);
    if (funcs != null) {
      newNode.possibleOverloads = (funcs as KindArray).array;
    } else {
      let type = this._nameContext.get('Type', newNode.name) as HmType | null;
      if (type == null) {
        throw new WTypeError(newNode.origin!.originString, `Cannot find any function or type named \'${newNode.name}\'`);
      }
      newNode.becomeCast(type!);
      newNode.possibleOverloads = (this._nameContext.get('Func', 'operator cast') as KindArray).array;
      if (newNode.possibleOverloads == null) {
        throw new WTypeError(newNode.origin!.originString, `Cannot find any operator cast implementations in cast to ${type.toString()}`);
      }
    }
    super.visitCallExpression(newNode);
  }
}

class NativeFunc extends Func {
  implementation: ((arr: EPtr[], right: CallExpression | null) => EPtr | null) | null;
  instantiateImplementation: (substitution: InstantiationSubstitution) => ImplementationDataType;
  visitImplementationData: (implementationData: ImplementationDataType, visitor: LateChecker | Visitor) => void;
  didLayoutStructsInImplementationData: (implementationData: ImplementationDataType) => void;
  constructor(
    origin: LexerToken,
    name: string,
    returnType: HmType,
    typeParameters: Array<TypeRef | TypeVariable>,
    parameters: Array<FuncParameter>,
    isCast: boolean,
    shaderType: null | string
  ) {
    super(origin, name, returnType, typeParameters, parameters, isCast, shaderType);
    this.implementation = null;
    this.instantiateImplementation = (substitution: InstantiationSubstitution): ImplementationDataType => {
      return new ImplementationDataType(new StructType(origin, '', []), '');
    };
    this.visitImplementationData = (implementationData: ImplementationDataType, visitor: LateChecker | Visitor): void => {};
    this.didLayoutStructsInImplementationData = (implementationData: ImplementationDataType): void => {};
    this.isRestricted = false;
  }

  get isNative(): boolean {
    return true;
  }

  toDeclString(): string {
    return `native ${super.toDeclString()}`;
  }
}

class NativeFuncInstance extends Func {
  private _func1: NativeFunc;
  private _implementationData?: ImplementationDataType;

  constructor(
    func1: NativeFunc,
    returnType: HmType,
    parameters: FuncParameter[],
    isCast: boolean,
    shaderType: string | null,
    implementationData: ImplementationDataType
  ) {
    super(func1.origin!, func1.name, returnType, [], parameters, isCast, shaderType);
    this._func1 = func1;
    this._implementationData = implementationData;
  }

  get func1(): NativeFunc {
    return this._func1;
  }

  get isNative(): boolean {
    return true;
  }

  get implementationData(): ImplementationDataType {
    return this._implementationData!;
  }

  toDeclString(): string {
    return `native ${super.toDeclString()}`;
  }
}

class NativeType extends HmType {
  private _isNumber: boolean = false;
  private _isInt: boolean = false;
  private _isFloating: boolean = false;
  private _isPrimitive: boolean = false;
  isSigned: boolean = false;
  defaultValue: number = 0;
  createLiteral: (origin: LexerToken, value: number) => GenericLiteral = (origin: LexerToken | null, value: number): GenericLiteral =>
    new GenericLiteral(origin!, value);
  successorValue: (value: number) => number = (value: number) => value;
  valuesEqual: (a: number, b: number) => boolean = (a: number, b: number) => false;
  populateDefaultValue: (buffer: EBuffer, offset: number) => void = (buffer: EBuffer, offset: number): void => {};
  formatValueFromIntLiteral: (value: number) => number = (value: number) => value;
  formatValueFromUintLiteral: (value: number) => number = (value: number) => value;
  allValues: () => Array<ResultMemberInfo> = () => new Array<ResultMemberInfo>();
  formatValueFromFloatLiteral: (value: number) => number = (value: number) => value;
  formatValueFromDoubleLiteral: (value: number) => number = (value: number) => value;
  formatValueFrom: (value: number) => number = (value: number) => {
    return value;
  };

  constructor(origin: LexerToken | null, name: string, typeParameters: Node[]) {
    super();
    if (!(typeParameters instanceof Array)) {
      throw new Error(`type parameters not array: ${typeParameters}`);
    }
    this.origin = origin;
    this._isNumber = false;
    this._isInt = false;
    this._isFloating = false;
    this._isPrimitive = false;
    this.name = name;
    this.typeParameters = typeParameters;
  }

  get isNative(): boolean {
    return true;
  }

  // We let Intrinsics.js set these as it likes.
  get isNumber(): boolean {
    return this._isNumber;
  }

  set isNumber(newValue: boolean) {
    this._isNumber = newValue;
  }

  get isInt(): boolean {
    return this._isInt;
  }

  set isInt(newValue: boolean) {
    this._isInt = newValue;
  }

  get isFloating(): boolean {
    return this._isFloating;
  }

  set isFloating(newValue: boolean) {
    this._isFloating = newValue;
  }

  get isPrimitive(): boolean {
    return this._isPrimitive;
  }
  set isPrimitive(newValue: boolean) {
    this._isPrimitive = newValue;
  }

  instantiate(typeArguments: TypeVariable[]): HmType {
    if (typeArguments.length !== this.typeParameters.length) {
      throw new Error('Wrong number of type arguments to instantiation');
    }
    if (typeArguments.length === 0) {
      return this;
    }
    return new NativeTypeInstance(this, typeArguments);
  }

  toString(): string {
    let arr = this.typeParameters.map((typeParameter: Node): string => {
      return typeParameter.toString();
    });
    return `native typedef ${this.name}<${arr.join('')}>`;
  }
}

class NativeTypeInstance extends HmType {
  constructor(type: Node, typeArguments: TypeVariable[]) {
    super();
    this.hmType = type as HmType;
    this.typeArguments = typeArguments;
  }

  get isPrimitive(): boolean {
    return this._hmType!.isPrimitive;
  }

  get isNative(): boolean {
    return true;
  }

  unifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    if (this.hmType !== other.hmType) {
      return false;
    }
    if (this.typeArguments!.length !== (other as HmType).typeArguments!.length) {
      return false;
    }
    for (let i = 0; i < this.typeArguments!.length; i++) {
      if (!this.typeArguments![i].unify(unificationContext, (other as HmType).typeArguments![i])) {
        return false;
      }
    }
    return true;
  }

  toString(): string {
    return `${this.hmType!.toString()}<${this.typeArguments}>`;
  }
}

class NormalUsePropertyResolver extends Rewriter {
  visitDotExpression2(node: Node): Expression {
    return super.visitDotExpression(node as DotExpression)!.rewriteAfterCloning();
  }

  visitIndexExpression2(node: Node): Expression {
    return super.visitIndexExpression(node as IndexExpression).rewriteAfterCloning();
  }
  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    let tmp: Map<string, (node: Base) => void | Base | string | null> = super.mapReturn() as Map<string, (node: Base) => void | Base | null>;
    tmp.set('visitDotExpression', this.visitDotExpression2 as (node: Base) => Expression);
    tmp.set('visitIndexExpression', this.visitIndexExpression2 as (node: Base) => Expression);
    return tmp;
  }
}

class NullLiteral extends Expression {
  constructor(origin: LexerToken) {
    super(origin);
    this.hmType = new NullType(origin);
  }

  toString(): string {
    return 'null';
  }
}

class NullType extends HmType {
  constructor(origin: LexerToken) {
    super();
    this.origin = origin;
  }

  // For now we answer false for isPtr and isArrayRef because that happens to make all of the logic
  // work. But, it's strange, since as a bottom type it really is the case that this is both isPtr and
  // isArrayRef.

  get isPrimitive(): boolean {
    return true;
  }

  get isUnifiable(): boolean {
    return true;
  }

  get isLiteral(): boolean {
    return true;
  }

  typeVariableUnify(unificationContext: UnificationContext, other: Node): boolean {
    if (!(other instanceof HmType)) {
      return false;
    }

    return this.typeVariableUnifyImpl(unificationContext, other);
  }

  unifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    return this.typeVariableUnify(unificationContext, other as HmType);
  }

  verifyAsArgument(unificationContext: UnificationContext): VerifyResult {
    let realThis: HmType = unificationContext.find(this) as HmType;
    if (realThis.isPtr || realThis.isArrayRef) {
      return new VerifyResult(true);
    }
    return new VerifyResult(false, `Null cannot be used with non-pointer type ${realThis}`);
  }

  verifyAsParameter(unificationContext: UnificationContext): VerifyResult;
  verifyAsParameter(unificationContext: UnificationContext): void;
  verifyAsParameter(unificationContext: UnificationContext): VerifyResult | void {
    throw new Error('NullType should never be used as a type parameter');
  }

  commitUnification(unificationContext: UnificationContext): void {
    this.hmType = unificationContext.find(this) as ReferenceType | null;
  }

  toString(): string {
    return 'nullType';
  }
}

class OverloadResolutionFailure {
  private _function: Func;
  private _reason: string;

  constructor(func: Func, reason: string) {
    this._function = func;
    this._reason = reason;
  }

  get func(): Func {
    return this._function;
  }

  get reason(): string {
    return this._reason;
  }

  toString(): string {
    return `${this.func.toDeclString()} did not match because: ${this.reason}`;
  }
}

let prepare = ((): ((origin: string, lineNumberOffset: number, text: string) => Program) => {
  let standardProgram: Program;
  return (origin: string, lineNumberOffset: number, text: string): Program => {
    if (standardProgram == null) {
      standardProgram = new Program();
      let firstLineOfStandardLibrary = 28; // See StandardLibrary.js.
      forMaxDepth();
      parse(standardProgram, '/internal/stdlib', 'native', firstLineOfStandardLibrary - 1, standardLibrary);
    }

    let program: Program = cloneProgram(standardProgram);
    if (true) {
      parse(program, origin, 'user', lineNumberOffset, text);
      program = programWithUnnecessaryThingsRemoved(program);
    }

    foldConstexprs(program);
    let nameResolver: NameResolver = createNameResolver(program);
    resolveNamesInTypes(program, nameResolver);
    resolveNamesInProtocols(program, nameResolver);
    resolveTypeDefsInTypes(program);
    resolveTypeDefsInProtocols(program);
    checkRecursiveTypes(program);
    synthesizeStructAccessors(program);
    synthesizeEnumFunctions(program);
    resolveNamesInFunctions(program, nameResolver);
    resolveTypeDefsInFunctions(program);
    flattenProtocolExtends(program);
    check(program);
    checkLiteralTypes(program);
    resolveProperties(program);
    findHighZombies(program);
    checkLiteralTypes(program);
    checkProgramWrapped(program);
    checkReturns(program);
    checkUnreachableCode(program);
    checkLoops(program);
    checkRecursion(program);
    checkProgramWrapped(program);
    findHighZombies(program);
    inline(program);
    return program;
  };
})();

class Program extends Node {
  private _funcInstantiator: FuncInstantiator | null;
  private _globalNameContext: NameContext;
  intrinsics: Intrinsics;
  private _functions: Map<string, Array<Func>>;
  private _types: Map<string, HmType>;
  private _protocols: Map<string, HmProtocol>;
  private _topLevelStatements: Array<Node>;

  constructor() {
    super();
    this._topLevelStatements = [];
    this._functions = new Map<string, Array<Func>>();
    this._types = new Map<string, HmType>();
    this._protocols = new Map<string, HmProtocol>();
    this._globalNameContext = new NameContext();
    this._globalNameContext.recognizeIntrinsics();
    this.intrinsics = this._globalNameContext.intrinsics!;
    this._funcInstantiator = new FuncInstantiator(this);
    this._globalNameContext.program = this;
  }

  get topLevelStatements(): Array<Node> {
    return this._topLevelStatements;
  }

  get functions(): Map<string, Array<Func>> {
    return this._functions;
  }

  get types(): Map<string, HmType> {
    return this._types;
  }

  get protocols(): Map<string, HmProtocol> {
    return this._protocols;
  }

  get funcInstantiator(): FuncInstantiator {
    return this._funcInstantiator!;
  }

  get globalNameContext(): NameContext {
    return this._globalNameContext;
  }

  add(statement: Node): void {
    statement.program = this;
    if (statement instanceof Func) {
      let array: Func[] | null = this._functions.get(statement.name) ?? null;
      if (array == null) {
        array = [];
      }
      array!.push(statement);
      this._functions.set(statement.name, array);
    } else if (statement instanceof HmType) {
      this._types.set(statement.name, statement as HmType);
    } else if (statement instanceof HmProtocol) {
      this._protocols.set(statement.name!, statement as HmProtocol);
    } else {
      throw new Error(`Statement is not a function or type: ${statement.toString()}`);
    }
    this._topLevelStatements.push(statement);
    this._globalNameContext.add(statement);
  }

  toString(): string {
    if (this._topLevelStatements.length === 0) {
      return '';
    }
    return this._topLevelStatements.map(($0) => $0.toString()).join(';') + ';';
  }
}

class PropertyResolver extends Visitor {
  visitRValuesWithinLValue(node: Node): void {
    let visit = (node: Node): NativeType => {
      return node.visit(this) as NativeType;
    };
    node.visit(new RValueFinder(visit));
  }

  visitPropertyAccess(node: PropertyAccessExpression): void {
    let newNode = node.visit(new NormalUsePropertyResolver()) as Node;
    newNode.visit(this);
    node.become(newNode as Expression);
  }

  visitDotExpression(node: Node): void {
    this.visitPropertyAccess(node as DotExpression);
  }

  visitIndexExpression(node: Node): void {
    this.visitPropertyAccess(node as IndexExpression);
  }

  handleReadModifyWrite(node: ReadModifyWriteExpression): void {
    let type: HmType | null = node.oldValueVar.hmType;
    if (type == null) {
      throw new Error('Null type');
    }
    let simpleLHS = node.lValue!.visit(new NormalUsePropertyResolver()) as Expression;
    if (simpleLHS.isLValue) {
      if (simpleLHS.addressSpace == null) {
        throw new Error(node.origin!.originString + `: LHS without address space: ${simpleLHS.toString()}`);
      }
      let ptrType: PtrType = new PtrType(node.origin!, simpleLHS.addressSpace!, type as TypeRef);
      let ptrVar: AnonymousVariable = new AnonymousVariable(node.origin!, ptrType);
      node.become(
        new CommaExpression(node.origin!, [
          node.oldValueVar, node.newValueVar, ptrVar,
		  new Assignment(node.origin!, VariableRef.wrap(ptrVar) as Expression, new MakePtrExpression(node.origin!, simpleLHS as HmValue), ptrType), new Assignment(
            node.origin!,
            node.oldValueRef(),
            new DereferenceExpression(node.origin!, VariableRef.wrap(ptrVar) as Expression, type as TypeRef, simpleLHS.addressSpace),
            type
          ), new Assignment(node.origin!, node.newValueRef(), node.newValueExp!, type), new Assignment(
            node.origin!,
            new DereferenceExpression(node.origin!, VariableRef.wrap(ptrVar) as Expression, type as TypeRef, simpleLHS.addressSpace),
            node.newValueRef(),
            type
          ), node.resultExp!
        ])
      );
      return;
    }

    let result: ReadModifyWriteExpression = new ReadModifyWriteExpression(
      node.origin!,
      (node.lValue as PropertyAccessExpression).base,
      (node.lValue as PropertyAccessExpression).baseType as TypeRef
    );
    result.newValueExp = new CommaExpression(node.origin!,
	 [node.oldValueVar, node.newValueVar,
   	  new Assignment(node.origin!, node.oldValueRef(), (node.lValue as PropertyAccessExpression).emitGet(result.oldValueRef()), type),
	    new Assignment(node.origin!, node.newValueRef(), node.newValueExp!, type),
	    (node.lValue as PropertyAccessExpression).emitSet(result.oldValueRef(), node.newValueRef())
      ]);
    result.resultExp = node.newValueRef();
    this.handleReadModifyWrite(result);
    node.become(result);
  }

  visitReadModifyWriteExpression(node: Node): void {
    let newNode = node as ReadModifyWriteExpression;
    newNode.newValueExp!.visit(this);
    newNode.resultExp!.visit(this);
    this.visitRValuesWithinLValue(newNode.lValue!);
    this.handleReadModifyWrite(newNode);
  }

  visitAssignment(node: Node): void {
    let newNode = node as Assignment;
    this.visitRValuesWithinLValue(newNode.lhs);
    newNode.rhs.visit(this);
    let simpleLHS: Expression = newNode.lhs.visit(new NormalUsePropertyResolver()) as Expression;
    if (simpleLHS.isLValue) {
      newNode.lhs.become(simpleLHS);
      return;
    }
    if (!(newNode.lhs instanceof PropertyAccessExpression)) {
      throw new Error(`Unexpected lhs type: ${newNode.lhs.constructor.name}`);
    }
    let result: ReadModifyWriteExpression = new ReadModifyWriteExpression(
      newNode.origin!,
      (newNode.lhs as PropertyAccessExpression).base,
      (newNode.lhs as PropertyAccessExpression).baseType as TypeRef
    );
    let resultVar: AnonymousVariable = new AnonymousVariable(newNode.origin!, newNode.hmType);
    result.newValueExp = new CommaExpression(newNode.origin!,
      [resultVar, new Assignment(newNode.origin!, VariableRef.wrap(resultVar) as Expression, newNode.rhs, newNode.hmType),
        (newNode.lhs as PropertyAccessExpression).emitSet(result.oldValueRef(), VariableRef.wrap(resultVar))
      ]);
    result.resultExp = VariableRef.wrap(resultVar);
    this.handleReadModifyWrite(result);
    newNode.become(result);
  }

  visitMakePtrExpression(node: Node): void {
    let newNode = node as MakePtrExpression;
    super.visitMakePtrExpression(newNode);
    if (!newNode.lValue!.isLValue) {
      throw new WTypeError(newNode.origin!.originString, `Not an lvalue: ${newNode.lValue!.toString()}`);
    }
  }

  visitMakeArrayRefExpression(node: Node): void {
    let newNode = node as MakeArrayRefExpression;
    super.visitMakeArrayRefExpression(node);
    if (!newNode.lValue!.isLValue) {
      throw new WTypeError(newNode.origin!.originString, `Not an lvalue: ${newNode.lValue!.toString()}`);
    }
  }
}

class ProtocolFuncDecl extends Func {
  possibleOverloads: Func[] = [];
  get typeParametersForCallResolution(): Node[] {
    return this.typeParameters.concat(this.protocolDecl!.typeVariable);
  }
}

class ProtocolRef extends HmProtocol {
  protocolDecl: ProtocolDecl | null;

  constructor(origin: LexerToken, name: string) {
    super(origin, name);
    this.protocolDecl = null;
  }

  inherits(other: ProtocolRef): VerifyResult {
    return this.protocolDecl!.inherits(other);
  }

  hasHeir(type: HmType): VerifyResult {
    return this.protocolDecl!.hasHeir(type);
  }

  get isPrimitive(): boolean {
    return this.protocolDecl!.isPrimitive;
  }
}

class PtrType extends ReferenceType {
  constructor(origin: LexerToken | null, addressSpace: string, elementType: HmType) {
    super(origin, addressSpace, elementType);
  }

  get isPtr(): boolean {
    return true;
  }

  unifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    let newOther = other as HmType;
    if (!newOther.isPtr) {
      return false;
    }
    if (this.addressSpace !== newOther.addressSpace) {
      return false;
    }
    return this.elementType!.unify(unificationContext, newOther.elementType!);
  }

  argumentForAndOverload(origin: LexerToken, value: HmValue): HmValue;
  argumentForAndOverload(origin: LexerToken, value: HmValue): void;
  argumentForAndOverload(origin: LexerToken, value: HmValue): HmValue | void {
    throw new WTypeError(origin.originString, 'Pointer subscript is not valid');
  }

  argumentTypeForAndOverload(origin: LexerToken): ReferenceType;
  argumentTypeForAndOverload(origin: LexerToken, type: HmType | null): void;
  argumentTypeForAndOverload(origin: LexerToken, type?: HmType | null): ReferenceType | void {
    throw new WTypeError(origin.originString, 'Pointer subscript is not valid');
  }

  returnTypeFromAndOverload(origin: LexerToken): TypeRef {
    return this.elementType! as TypeRef;
  }

  toString(): string {
    return `${this.elementType?.toString() ?? ''}* ${this.addressSpace}`;
  }
}

class ReadModifyWriteExpression extends Expression {
  oldValueVar: AnonymousVariable;
  newValueVar: AnonymousVariable;
  newValueExp: null | Expression;
  resultExp: null | VariableRef;

  constructor(origin: LexerToken, lValue: Expression, type: null | TypeRef = null) {
    super(origin);
    this.lValue = lValue;
    this.oldValueVar = new AnonymousVariable(origin, type);
    this.newValueVar = new AnonymousVariable(origin, type);
    this.newValueExp = null;
    this.resultExp = null;
  }

  oldValueRef(): VariableRef {
    return VariableRef.wrap(this.oldValueVar);
  }

  newValueRef(): VariableRef {
    return VariableRef.wrap(this.newValueVar);
  }

  toString(): string {
    if (this.isBecome) {
      return `(${this.target.toString()})`;
    } else {
      return `RMW(${this.lValue!.toString()}, ${this.oldValueVar.toString()}, ${this.newValueVar.toString()}, ${this.newValueExp?.toString()}, ${this.resultExp?.toString()})`;
    }
  }
}

class RecursionChecker extends Visitor {
  private _visiting: VisitingSet;

  constructor() {
    super();
    this._visiting = new VisitingSet();
  }

  visitFuncDef(node: Node): void {
    this._visiting.doVisit(node, (): void => super.visitFuncDef(node));
  }

  visitCallExpression(node: Node): void {
    (node as CallExpression).func1!.visit(this);
  }
}

class RecursiveTypeChecker extends Visitor {
  private _visiting: VisitingSet;

  constructor() {
    super();
    this._visiting = new VisitingSet();
  }

  visitFuncDef(node: Node): void {}

  visitNativeFunc(node: Node): void {}

  visitStructType(node: Node): void {
    let nodes = node as StructType;
    this._visiting.doVisit(nodes, (): void => {
      super.visitStructType(nodes);
    });
  }

  visitReferenceType(node: Node): void {}

  visitTypeRef(node: Node): void {
    super.visitTypeRef(node);
    node.hmType!.visit(this);
  }
}

class Return extends Node {
  private _value: HmValue | null;
  func1: FuncDef | null = null;

  constructor(origin: LexerToken, value: HmValue | null) {
    super();
    this.origin = origin;
    this._value = value;
  }

  get value(): HmValue | null {
    return this._value;
  }

  toString(): string {
    if (this.value != null) {
      return `return ${this.value}`;
    }
    return 'return';
  }
}

class ReturnChecker extends Visitor {
  private _program: Program;
  returnStyle: ReturnStyleType;
  constructor(program: Program) {
    super();
    this._program = program;
    this.returnStyle = new ReturnStyleType();
    this.returnStyle.definitelyReturns = 'Definitely Returns';
    this.returnStyle.definitelyDoesntReturn = 'Definitely Doesn`t Return';
    this.returnStyle.hasntReturnedYet = 'Hasn`t Returned Yet';
  }

  private mergeReturnStyle(a: string | null, b: string | null): string {
    if (a == null) {
      return b!;
    }
    if (b == null) {
      return a!;
    }
    switch (a) {
      case this.returnStyle.definitelyReturns:
      case this.returnStyle.definitelyDoesntReturn:
        if (a === b) {
          return a!;
        }
        return this.returnStyle.hasntReturnedYet;
      case this.returnStyle.hasntReturnedYet:
        return this.returnStyle.hasntReturnedYet;
      default:
        throw new Error(`Bad return style: ${a!}`);
    }
  }

  visitFuncDef(node: Node): void {
    let newNode = node as FuncDef;
    if (newNode.returnType.equals(newNode.program!.intrinsics.void!) != null) {
      return;
    }
    let bodyValue: string = newNode.body.visit(this)!.toString();
    if (bodyValue === this.returnStyle.definitelyDoesntReturn || bodyValue === this.returnStyle.hasntReturnedYet) {
      throw new WTypeError(newNode.origin!.originString, 'Function does not return');
    }
  }

  visitBlock2(node: Node): string {
    let newNode = node as Block;
    for (let statement of newNode.statements as Node[]) {
      switch (statement.visit(this)! as String) {
        case this.returnStyle.definitelyReturns:
          return this.returnStyle.definitelyReturns;
        case this.returnStyle.definitelyDoesntReturn:
          return this.returnStyle.definitelyDoesntReturn;
        case this.returnStyle.hasntReturnedYet:
          continue;
        default:
          break;
      }
    }
    return this.returnStyle.hasntReturnedYet;
  }

  visitIfStatement2(node: Node): string {
    let newNode = node as IfStatement;
    if (newNode.elseBody != null) {
      let bodyValue: string = newNode.body.visit(this) as string;
      let elseValue: string = newNode.elseBody.visit(this) as string;
      return this.mergeReturnStyle(bodyValue, elseValue);
    }
    return this.returnStyle.hasntReturnedYet;
  }

  private isBoolCastFromLiteralTrue(node: CallExpression): boolean {
    return (
      node.isCast &&
      node.returnType instanceof TypeRef &&
      node.returnType!.equals(this._program.intrinsics.bool!) !== null &&
      node.argumentList.length === 1 &&
      node.argumentList[0] instanceof BoolLiteral &&
      ((node.argumentList[0] as BoolLiteral).value as boolean)
    );
  }

  visitWhileLoop2(node: Node): string {
    let newNode = node as WhileLoop;
    let bodyReturn = newNode.body.visit(this);
    if (newNode.conditional instanceof CallExpression && this.isBoolCastFromLiteralTrue(newNode.conditional)) {
      switch (bodyReturn as string) {
        case this.returnStyle.definitelyReturns:
          return this.returnStyle.definitelyReturns;
        case this.returnStyle.definitelyDoesntReturn:
        case this.returnStyle.hasntReturnedYet:
          return this.returnStyle.hasntReturnedYet;
        default:
          break;
      }
    }
    return this.returnStyle.hasntReturnedYet;
  }

  visitDoWhileLoop2(node: Node): string {
    let newNode = node as DoWhileLoop;
    let result = this.returnStyle.hasntReturnedYet;
    switch (newNode.body!.visit(this) as string) {
      case this.returnStyle.definitelyReturns:
        return (result = this.returnStyle.definitelyReturns);
      case this.returnStyle.definitelyDoesntReturn:
      case this.returnStyle.hasntReturnedYet:
        return (result = this.returnStyle.hasntReturnedYet);
      default:
        break;
    }
    return result;
  }

  visitForLoop2(node: Node): string {
    let newNode = node as ForLoop;
    let bodyReturn = newNode.body.visit(this);
    if (newNode.condition === null || this.isBoolCastFromLiteralTrue(newNode.condition as CallExpression)) {
      switch (bodyReturn as string) {
        case this.returnStyle.definitelyReturns:
          return this.returnStyle.definitelyReturns;
        case this.returnStyle.definitelyDoesntReturn:
        case this.returnStyle.hasntReturnedYet:
          return this.returnStyle.hasntReturnedYet;
        default:
          break;
      }
    }
    return '';
  }

  visitSwitchStatement2(node: Node): string {
    let newNode = node as SwitchStatement;
    // FIXME: This seems like it's missing things. For example, we need to be smart about this:
    //
    // for (;;) {
    //     switch (...) {
    //     ...
    //         continue; // This continues the for loop
    //     }
    // }
    //
    // I'm not sure what that means for this analysis. I'm starting to think that the right way to
    // build this analysis is to run a visitor that builds a CFG and then analyze the CFG.
    // https://bugs.webkit.org/show_bug.cgi?id=177172

    let returnStyle: string | null = null;
    for (let switchCase of newNode.switchCases) {
      let bodyStyle: string = switchCase.body!.visit(this) as string;
      // FIXME: The fact that we need this demonstrates the need for CFG analysis.
      if (bodyStyle === this.returnStyle.definitelyDoesntReturn) {
        bodyStyle = this.returnStyle.hasntReturnedYet;
      }
      returnStyle = this.mergeReturnStyle(returnStyle, bodyStyle);
    }
    return returnStyle!;
  }

  visitReturn2(node: Node): string {
    return this.returnStyle.definitelyReturns;
  }

  visitTrapStatement2(node: Node): string {
    return this.returnStyle.definitelyReturns;
  }

  visitBreak2(node: Node): string {
    return this.returnStyle.definitelyDoesntReturn;
  }

  visitContinue2(node: Node): string {
    // FIXME: This seems wrong. Consider a loop like:
    //
    // int foo() { for (;;) { continue; } }
    //
    // This program shouldn't claim that the problem is that it doesn't return.
    // https://bugs.webkit.org/show_bug.cgi?id=177172
    return this.returnStyle.definitelyDoesntReturn;
  }

  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    let tmp: Map<string, (node: Base) => void | Base | string | null> = super.mapReturn();
    tmp.set('visitContinue', this.visitContinue2 as (node: Base) => string);
    tmp.set('visitBreak', this.visitBreak2 as (node: Base) => string);
    tmp.set('visitTrapStatement', this.visitTrapStatement2 as (node: Base) => string);
    tmp.set('visitReturn', this.visitReturn2 as (node: Base) => string);
    tmp.set('visitSwitchStatement', this.visitSwitchStatement2 as (node: Base) => string);
    tmp.set('visitForLoop', this.visitForLoop2 as (node: Base) => string);
    tmp.set('visitDoWhileLoop', this.visitDoWhileLoop2 as (node: Base) => string);
    tmp.set('visitWhileLoop', this.visitWhileLoop2 as (node: Base) => string);
    tmp.set('visitIfStatement', this.visitIfStatement2 as (node: Base) => string);
    tmp.set('visitBlock', this.visitBlock2 as (node: Base) => string);
    return tmp;
  }
}

class ReturnException extends Error {
  _value: EPtr | null;

  constructor(value: EPtr | null) {
    super();
    this._value = value;
  }

  get value(): EPtr | null {
    return this._value;
  }
}

class StatementCloner extends Rewriter {
  visitFuncDef(node: Node): FuncDef {
    let newNode = node as FuncDef;
    let typeParameters: Node[] = newNode.typeParameters.map((typeParameter: Node): Node => {
      return typeParameter.visit(this) as Node;
    });
    let result: FuncDef = new FuncDef(
      newNode.origin!,
      newNode.name,
      newNode.returnType.visit(this) as HmType,
      typeParameters,
      newNode.parameters.map((parameter: FuncParameter): FuncParameter => {
        return parameter.visit(this) as FuncParameter;
      }),
      newNode.body.visit(this) as Block,
      newNode.isCast,
      newNode.shaderType
    );
    result.isRestricted = newNode.isRestricted;
    return result;
  }

  visitNativeFunc(node: Node): NativeFunc {
    let newNode = node as NativeFunc;
    let result: NativeFunc = new NativeFunc(
      newNode.origin!,
      newNode.name,
      newNode.returnType.visit(this) as HmType,
      newNode.typeParameters.map((typeParameter: Node): TypeVariable => {
        return typeParameter.visit(this) as TypeVariable;
      }),
      newNode.parameters.map((parameter: FuncParameter): FuncParameter => {
        return parameter.visit(this) as FuncParameter;
      }),
      newNode.isCast,
      newNode.shaderType
    );
    result.isRestricted = newNode.isRestricted;
    return result;
  }

  visitNativeType(node: Node): NativeType {
    let newNode = node as NativeType;
    return new NativeType(
      newNode.origin,
      newNode.name,
      newNode.typeParameters.map((typeParameter: Node): Node => {
        return typeParameter.visit(this) as Node;
      })
    );
  }

  visitTypeDef(node: Node): TypeDef {
    let newNode = node as TypeDef;
    return new TypeDef(
      newNode.origin!,
      newNode.name,
      newNode.typeParameters.map((typeParameter: Node): Node => {
        return typeParameter.visit(this) as Node;
      }),
      newNode.hmType!.visit(this) as TypeRef
    );
  }

  visitStructType(node: Node): StructType {
    let newNode = node as StructType;
    let result = new StructType(
      newNode.origin!,
      newNode.name,
      newNode.typeParameters.map((typeParameter: Node): TypeVariable => {
        return typeParameter.visit(this) as TypeVariable;
      })
    );
    for (let field of newNode.fields) {
      result.add(field.visit(this) as Field);
    }
    return result;
  }

  visitConstexprTypeParameter(node: Node): ConstexprTypeParameter {
    let newNode = node as ConstexprTypeParameter;
    return new ConstexprTypeParameter(newNode.origin!, newNode.name, newNode.hmType!.visit(this) as TypeRef);
  }

  visitProtocolDecl(node: Node): ProtocolDecl {
    let newNode = node as ProtocolDecl;
    let result: ProtocolDecl = new ProtocolDecl(newNode.origin!, newNode.name);
    for (let hmProtocol of newNode.extends) {
      result.addExtends(hmProtocol.visit(this) as ProtocolRef);
    }
    for (let signature of newNode.signatures) {
      result.add(signature.visit(this) as ProtocolFuncDecl);
    }
    return result;
  }

  visitTypeVariable(node: Node): TypeVariable {
    let newNode = node as TypeVariable;
    return new TypeVariable(newNode.origin, newNode.name, Node.visit(newNode.protocol1, this) as ProtocolRef);
  }

  visitProtocolRef(node: Node): ProtocolRef {
    let newNode = node as ProtocolRef;
    return new ProtocolRef(newNode.origin!, newNode.name);
  }

  visitBoolLiteral(node: Node): BoolLiteral {
    let newNode = node as BoolLiteral;
    return new BoolLiteral(newNode.origin!, newNode.value as boolean);
  }

  visitTypeOrVariableRef(node: Node): TypeOrVariableRef {
    let newNode = node as TypeOrVariableRef;
    return new TypeOrVariableRef(newNode.origin!, newNode.name);
  }

  visitEnumType(node: Node): EnumType {
    let newNode = node as EnumType;
    let result: EnumType = new EnumType(newNode.origin!, newNode.name, newNode.baseType.visit(this) as HmType);
    for (let member of newNode.members) {
      result.add(member);
    }
    return result;
  }
}

class StructLayoutBuilder extends Visitor {
  private _offset: number;

  constructor() {
    super();
    this._offset = 0;
  }

  visitReferenceType(node: Node): void {}

  visitStructType(node: Node): void {
    let nodes = node as StructType;
    if (nodes.size !== -1) {
      return;
    }
    if (nodes.typeParameters.length !== 0) {
      throw new Error(`Cannot do layout for generic type: ${nodes.toString()}`);
    }
    let oldOffset = this._offset;
    this._offset = 0;
    super.visitStructType(nodes);
    nodes.size = this._offset;
    this._offset += oldOffset;
  }

  get offset(): number {
    return this._offset;
  }

  visitField(node: Node): void {
    let newNode = node as Field;
    super.visitField(newNode);
    newNode.offset = this._offset;
    let size: number | null = newNode.hmType!.size;
    if (size === -1) {
      throw new Error(`Type does not have size: ${newNode.hmType!.toString()}`);
    }
    this._offset += size!;
  }

  visitNativeFuncInstance(node: Node): void {
    let newNode = node as NativeFuncInstance;
    super.visitNativeFuncInstance(newNode);
    newNode.func1.didLayoutStructsInImplementationData(newNode.implementationData);
  }

  visitTypeRef(node: Node): void {
    super.visitTypeRef(node);
    node.hmType!.visit(this);
  }

  visitCallExpression(node: Node): void {
    let newNode = node as CallExpression;
    for (let argument of newNode.argumentList) {
      Node.visit(argument, this);
    }
    let handleTypeArguments = (actualTypeArguments: (Node | null)[] | null): void => {
      if (actualTypeArguments !== null) {
        for (let argument of actualTypeArguments!) {
          argument!.visit(this);
        }
      }
    };
    handleTypeArguments(newNode.instantiatedActualTypeArguments);
    Node.visit(newNode.nativeFuncInstance, this);
    Node.visit(newNode.resultType, this);
  }
}

class StructType extends HmType {
  private _fields: Map<string, Field>;

  get fields(): Field[] {
    {
      let tmp: Field[] = [];
      for (const ele of this._fields) {
        tmp.push(ele[1]);
      }
      return tmp;
    }
  }

  get isPrimitive(): boolean {
    {
      let result = 1;
      for (let field of this.fields) {
        result = result & (field.hmType!.isPrimitive ? 1 : 0);
      }
      return result === 1;
    }
  }

  constructor(origin: LexerToken, name: string, typeParameters: Node[]) {
    super();
    this.origin = origin;
    this._fields = new Map<string, Field>();
    this.name = name;
    this.typeParameters = typeParameters;
  }

  add(field: Field): void {
    field.struct1 = this;
    let names: string[] = [];
    for (const ele of this._fields) {
      names.push(ele[0]);
    }
    if (names.includes(field.name)) {
      throw new WTypeError(field.origin!.originString, `Duplicate field name: ${field.name}`);
    }
    this._fields.set(field.name, field);
  }

  fieldByName(name: string): Field {
    return this._fields.get(name)!;
  }

  instantiate(typeArguments: Array<Node> | null = null): StructType {
    let substitution: Substitution | null = null;
    let typeParameters: Node[] = this.typeParameters;
    if (typeArguments != null) {
      if ((typeArguments?.length ?? 0) !== this.typeParameters.length) {
        throw new WTypeError(this.origin!.originString, 'Wrong number of type arguments to instantiation');
      }
      substitution = new Substitution(this.typeParameters, typeArguments!);
      typeParameters = [];
    }
    let instantiateImmediates: InstantiateImmediates = new InstantiateImmediates();
    let result = new StructType(this.origin!, this.name, typeParameters);
    for (let field of this.fields) {
      let newField: Field = field;
      if (substitution != null) {
        newField = newField.visit(substitution) as Field;
      }
      newField = newField.visit(instantiateImmediates) as Field;
      result.add(newField);
    }
    return result;
  }

  populateDefaultValue(buffer: EBuffer, offset: number): void {
    if (this.size === -1) {
      throw new Error(`Struct does not have layout: ${this.toString()} ${this}`);
    }
    for (let field of this.fields) {
      field.hmType!.populateDefaultValue(buffer, offset + field.offset);
    }
  }

  toString(): string {
    return `struct ${this.name}<${this.typeParameters + '> { ' + this.fields.map(($0: Field): string => $0.toString()).join('; ')}; }`;
  }
}

class Substitution extends Rewriter {
  private _map: Map<Node, Node>;

  constructor(parameters: Array<Node>, argumentList: Array<Node>) {
    super();
    this._map = new Map<Node, Node>();
    if (parameters.length !== argumentList.length) {
      throw new Error('Parameters and arguments are mismatched');
    }
    for (let i = 0; i < parameters.length; i++) {
      this._map.set(parameters[i], argumentList[i]);
    }
  }

  get map(): Map<Node, Node> {
    return this._map;
  }

  visitTypeRef2(node: Node): HmType | null {
    let newNode = node as TypeRef;
    let replacement = this._map.get((newNode as TypeRef).hmType!);
    if (replacement != null) {
      if (newNode.typeArguments!.length > 0) {
        throw new Error('Unexpected type arguments on type variable');
      }
      let result: HmType | null = replacement!.visit(new AutoWrapper()) as HmType | null;
      return result;
    }
    let result: TypeRef | null = super.visitTypeRef(newNode);
    return result;
  }

  visitVariableRef2(node: Node): Expression {
    let newNode = node as VariableRef;
    let replacement = this._map.get(newNode.variable!);
    if (replacement != null) {
      return replacement!.visit(new AutoWrapper()) as Expression;
    }
    return super.visitVariableRef(newNode)!;
  }

  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    let tmp: Map<string, (node: Base) => void | Base | string | null> = super.mapReturn();
    tmp.set('visitTypeRef', this.visitTypeRef2 as (node: Base) => HmType | null);
    tmp.set('visitVariableRef', this.visitVariableRef2 as (node: Base) => Expression);
    return tmp;
  }
}

class SwitchCase extends Node {
  private _value: Expression | null;

  constructor(origin: LexerToken, value: Expression | null, body: Block) {
    super();
    this.origin = origin;
    this._value = value;
    this.body = body;
  }

  get isDefault(): boolean {
    return this._value == null;
  }

  get value(): Expression | null {
    return this._value;
  }

  toString(): string {
    let result = '';
    if (this.isDefault) {
      result += 'default';
    } else {
      result += `cast ${this.value!.toString()}`;
    }
    return `${result}: ${this.body!.toString()}`;
  }
}

class SwitchStatement extends Node {
  private _value: HmValue;
  private _switchCases: SwitchCase[];

  constructor(origin: LexerToken, value: HmValue) {
    super();
    this.origin = origin;
    this._value = value;
    this._switchCases = [];
  }

  get value(): HmValue {
    return this._value;
  }

  add(switchCase: SwitchCase): void {
    this._switchCases.push(switchCase);
  }

  get switchCases(): SwitchCase[] {
    return this._switchCases;
  }

  toString(): string {
    let result = `switch (${this.value.toString()}) { `;
    if (this.switchCases.length !== 0) {
      result += this.switchCases
        .map((s: SwitchCase): string => {
          return s.toString();
        })
        .join('; ');
    }
    return result + '}';
  }
}

class TrapStatement extends Node {
  constructor(origin: LexerToken) {
    super();
    this.origin = origin;
  }

  toString(): string {
    return 'trap';
  }
}

class TypeDef extends HmType {
  constructor(origin: LexerToken, name: string, typeParameters: Node[], type: HmType) {
    super();
    this.origin = origin;
    this.hmType = type;
    this.name = name;
    this.typeParameters = typeParameters;
  }

  toString(): string {
    return `typedef ${this.name}<${this.typeParameters}> = ${this.hmType?.toString()}`;
  }
}

class TypeDefResolver extends Visitor {
  private _visiting: VisitingSet;

  constructor() {
    super();
    this._visiting = new VisitingSet();
  }

  visitTypeRef(node: Node): void {
    const newNode = node as TypeRef;
    this._visiting.doVisit(newNode, () => {
      for (let typeArgument of newNode.typeArguments as Node[]) {
        typeArgument.visit(this);
      }
      if (newNode.hmType instanceof TypeDef) {
        let unificationContext: UnificationContext = new UnificationContext(newNode.hmType!.typeParameters);
        if (newNode.typeArguments!.length !== newNode.hmType!.typeParameters.length) {
          throw new Error('argument/parameter mismatch (should have been caught earlier)');
        }
        for (let i = 0; i < newNode.typeArguments!.length; i++) {
          newNode.typeArguments![i].unify(unificationContext, newNode.hmType!.typeParameters[i]);
        }
        let verificationResult: VerifyResult = unificationContext.verify();
        if (!verificationResult.result) {
          throw new WTypeError(
            newNode.origin!.originString,
            `Type reference to a type definition violates protocol constraints: ${verificationResult.reason}`
          );
        }
        let newType = newNode.hmType!.hmType!.substituteToUnification(newNode.hmType!.typeParameters, unificationContext);
        newType.visit(this);
        newNode.setTypeAndArguments(newType, []);
      }
    });
  }
}

class TypeOrVariableRef extends Node {
  constructor(origin: LexerToken, name: string) {
    super();
    this.name = name;
    this.origin = origin;
  }

  toString(): string {
    return this.name;
  }
}

class TypeParameterRewriter extends Base {
  visitConstexprTypeParameter(aNode: Base): ConstexprTypeParameter {
    let node = aNode as ConstexprTypeParameter;
    return new ConstexprTypeParameter(node.origin!, node.name, node.hmType?.visit(new Rewriter()) as TypeRef);
  }

  visitTypeVariable(aNode: Base): TypeVariable {
    let node = aNode as TypeVariable;
    return new TypeVariable(node.origin, node.name, node.protocol1);
  }

  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    let tmp: Map<string, (node: Base) => void | Base | string | null> = super.mapReturn();
    tmp.set('visitConstexprTypeParameter', this.visitConstexprTypeParameter);
    tmp.set('visitTypeVariable', this.visitTypeVariable);
    return tmp;
  }
}

class TypeRef extends HmType {
  constructor(origin: LexerToken, name: string, typeArguments: Node[]) {
    super();
    this.origin = origin;
    this.name = name;
    this.hmType = null;
    this.typeArguments = typeArguments;
  }

  static wrap(type: HmType): TypeRef {
    if (type instanceof TypeRef && type.typeArguments.length === 0) {
      return type as TypeRef;
    }
    const name = type.name;
    let result = new TypeRef(type.origin!, name, []);
    result.hmType = type;
    return result;
  }

  static instantiate(type: StructType, typeArguments: HmType[]): TypeRef {
    let result = new TypeRef(type.origin!, type.name, typeArguments);
    result.hmType = type;
    return result;
  }

  get unifyNode(): Node {
    {
      if (this.typeArguments!.length === 0) {
        return this.hmType!.unifyNode;
      }
      return this;
    }
  }

  populateDefaultValue(buffer: EBuffer, offset: number): TypeRef {
    if (this.typeArguments!.length === 0) {
      return this.hmType!.populateDefaultValue(buffer, offset) as TypeRef;
    }
    throw new Error('Cannot get default value of a type instantiation');
  }

  get size(): number | null {
    {
      if (this.typeArguments!.length === 0) {
        return this.hmType!.size;
      }
      throw new Error('Cannot get size of a type instantiation');
    }
  }

  get isPrimitive(): boolean {
    {
      if (this.typeArguments!.length === 0) {
        return this.hmType?.isPrimitive ?? false;
      }
      throw new Error(`Cannot determine if an uninstantiated type is primitive: ${this}`);
    }
  }

  setTypeAndArguments(type: HmType, typeArguments: Node[]): void {
    this.name = '';
    this.hmType = type;
    this.typeArguments = typeArguments;
  }

  unifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    if (!(other instanceof TypeRef)) {
      return false;
    }
    if (!this.hmType!.unify(unificationContext, (other as TypeRef).hmType!)) {
      return false;
    }
    if (this.typeArguments!.length !== (other as TypeRef).typeArguments!.length) {
      return false;
    }
    for (let i = 0; i < this.typeArguments!.length; i++) {
      if (!this.typeArguments![i].unify(unificationContext, (other as TypeRef).typeArguments![i])) {
        return false;
      }
    }
    return true;
  }

  toString(): string {
    if (this.name === '') {
      return this.hmType!.toString();
    }
    if (this.typeArguments!.length === 0) {
      return this.name;
    }
    let arr = this.typeArguments!.map((filed: Node): string => {
      return filed.toString();
    });
    return `${this.name}<${arr.join(',')}>`;
  }
}

class TypeVariable extends HmType {
  private _protocol1: ProtocolRef | null;

  constructor(origin: LexerToken | null, name: string | null, protocol1: ProtocolRef | null) {
    super();
    this.origin = origin;
    this._protocol1 = protocol1;
    this.name = name!;
  }

  get protocol1(): ProtocolRef | null {
    return this._protocol1;
  }

  get isPrimitive(): boolean {
    return this._protocol1 != null && this._protocol1.isPrimitive;
  }

  get isUnifiable(): boolean {
    return true;
  }

  inherits(protocol1: ProtocolRef): VerifyResult {
    if (protocol1 == null) {
      return new VerifyResult(true);
    }
    if (this.protocol1 == null) {
      return new VerifyResult(false, `Type variable ${this.toString()} does not have a protocol`);
    }
    return this.protocol1!.inherits(protocol1);
  }

  typeVariableUnify(unificationContext: UnificationContext, other: Node): boolean {
    if (!(other instanceof HmType)) {
      return false;
    }
    return this.typeVariableUnifyImpl(unificationContext, other);
  }

  unifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    return this.typeVariableUnify(unificationContext, other);
  }

  verifyAsArgument(unificationContext: UnificationContext): VerifyResult {
    let realThis = unificationContext.find(this);

    // The thing we get unified with must be a type variable that accepts a broader set of
    // things than we do.
    if (!(realThis instanceof TypeVariable)) {
      return new VerifyResult(false, `Type variable argument ${this.toString()} cannot be passed to non-type-variable parameter type ${realThis.toString()}`);
    }
    let real = realThis as TypeVariable;
    if (this.protocol1 == null) {
      if (real.protocol1 != null) {
        return new VerifyResult(
          false,
          `Type variable without protocol ${this.toString()} cannot be passed to parameter type variable with protocol ${real.protocol1.toString()}`
        );
      }
      return new VerifyResult(true);
    }
    let result: VerifyResult = this.protocol1!.inherits(real.protocol1!);
    if (!result.result) {
      return new VerifyResult(
        false,
        `Protocol ${this.protocol1!.toString()} does not subsume protocol ${real.protocol1?.toString()} (passing type ${this.toString()}' to type ${real.toString()}): ${
          result.reason
        }`
      );
    }
    return new VerifyResult(true);
  }

  verifyAsParameter(unificationContext: UnificationContext): VerifyResult {
    if (this.protocol1 == null) {
      return new VerifyResult(true);
    }
    let realThis = unificationContext.find(this) as HmType;
    let result = realThis.inherits(this.protocol1);
    if (!result.result) {
      return new VerifyResult(
        false,
        `Type ${realThis.toString()} does not inherit protocol ${this.protocol1.toString()} (passing type ${realThis.toString()} to type ${this.toString()}): ${
          result.reason
        }`
      );
    }
    return new VerifyResult(true);
  }

  toString(): string {
    return this.name + (this.protocol1 != null ? `:${this.protocol1!.name}` : '');
  }
}

class TypeVariableTracker extends Visitor {
  private _set: Set<Node>;
  constructor() {
    super();
    this._set = new Set();
  }

  get set(): Set<Node> {
    return this._set;
  }

  consider(thing: Node): void {
    if (thing.isUnifiable) {
      this._set.add(thing);
    }
  }

  visitTypeRef(node: Node): void {
    let newNode = node as TypeRef;
    if (newNode.typeArguments!.length !== 0) {
      for (let typeArgument of newNode.typeArguments) {
        typeArgument.visit(this);
      }
      return;
    }
    this.consider(newNode.hmType!);
  }

  visitVariableRef(node: Node): void {
    this.consider((node as VariableRef).variable!);
  }
}

class TypedValue {
  private _type: HmType;
  private _ePtr: EPtr | null;

  constructor(type: HmType, ePtr: EPtr | null) {
    this._type = type;
    this._ePtr = ePtr;
  }

  get hmType(): HmType {
    return this._type;
  }

  get ePtr(): EPtr | null {
    return this._ePtr;
  }

  static box(type: HmType, value: number | null | boolean | EPtr | EArrayRef): TypedValue {
    return new TypedValue(type, EPtr.box(value));
  }

  get value(): null | number | boolean | EPtr | EArrayRef | EnumLiteral | undefined {
    return this.ePtr?.loadValue();
  }

  toString(): string {
    return `${this.hmType.toString()}(${this.ePtr?.toString() ?? 'null'})`;
  }
}

class UnificationContext {
  _typeParameters: Set<Node>;
  _nextMap: Map<Node, Node>;
  _extraNodes: Set<Node>;

  constructor(typeParameters: Node[]) {
    this._typeParameters = new Set(typeParameters);
    this._nextMap = new Map();
    this._extraNodes = new Set();
  }

  union(a: Node, b: Node): void {
    a = this.find(a);
    b = this.find(b);
    if (a === b) {
      return;
    }
    if (!a.isUnifiable) {
      let tmp = b;
      b = a;
      a = tmp;
      if (!a.isUnifiable) {
        throw new Error(`Cannot unify non-unifiable things ${a.toString()} and ${b.toString()}`);
      }
    }

    // Make sure that type parameters don't end up being roots.
    if (a.isUnifiable && b.isUnifiable && this._typeParameters.has(b)) {
      let tmp = b;
      b = a;
      a = tmp;
    }
    this._nextMap.set(a, b);
  }

  find(node: Node): Node {
    let currentNode = node;
    let nextNode = this._nextMap.get(currentNode) ?? null;
    if (nextNode == null) {
      return currentNode;
    }
    while (true) {
      currentNode = nextNode;
      nextNode = this._nextMap.get(currentNode) ?? null;
      if (nextNode == null) {
        break;
      }
    }
    this._nextMap.set(node, currentNode);
    return currentNode;
  }

  addExtraNode(node: Node): void {
    this._extraNodes.add(node);
  }

  get nodes(): Set<Node> {
    {
      let result = new Set<Node>();
      for (let item of this._nextMap) {
        result.add(item[0]);
        result.add(item[1]);
      }
      for (let node of this._extraNodes) {
        result.add(node);
      }
      return result;
    }
  }

  typeParameters(): Set<Node> {
    return this._typeParameters;
  }

  typeArguments(): Node[] {
    const tmpNodes: Node[] = [];
    for (let typeArgument of this.nodes) {
      if (!typeArgument.isUnifiable) {
        continue;
      }
      if (this._typeParameters.has(typeArgument)) {
        continue;
      }
      tmpNodes.push(typeArgument);
    }
    return tmpNodes;
  }

  verify(): VerifyResult {
    // We do a two-phase pre-verification. This gives literals a chance to select a more specific type.
    let preparations: (() => VerifyResult)[] = [];
    for (let node of this.nodes) {
      let preparation: (() => VerifyResult) | null = null;
      if (node instanceof GenericLiteralType) {
        preparation = (node as GenericLiteralType).prepareToVerify(this);
      }
      if (preparation != null) {
        preparations.push(preparation);
      }
    }
    for (let preparation of preparations) {
      let result = preparation();
      if (!result.result) {
        return result;
      }
    }
    for (let typeParameter of this._typeParameters) {
      let result = typeParameter.verifyAsParameter(this);
      if (!result.result) {
        return result;
      }
    }
    let numTypeVariableArguments = 0;
    let argumentSet: Set<Node> = new Set();
    for (let typeArgument of this.typeArguments()) {
      let result = typeArgument.verifyAsArgument(this);
      if (!result.result) {
        return result;
      }
      if (typeArgument.isLiteral) {
        continue;
      }
      argumentSet.add(this.find(typeArgument));
      numTypeVariableArguments += 1;
    }
    if (argumentSet.size === numTypeVariableArguments) {
      return new VerifyResult(true);
    }
    return new VerifyResult(false, 'Type variables used as arguments got unified with each other');
  }

  get conversionCost(): number {
    {
      let result = 0;
      for (let typeArgument of this.typeArguments()) {
        result += typeArgument.conversionCost(this);
      }
      return result;
    }
  }

  commit(): void {
    for (let typeArgument of this.typeArguments()) {
      typeArgument.commitUnification(this);
    }
  }
}

class UnreachableCodeChecker extends Visitor {
  private _returnChecker: ReturnChecker;
  constructor(program: Program) {
    super();
    this._returnChecker = new ReturnChecker(program);
  }

  visitBlock(node: Node): void {
    let newNode = node as Block;
    super.visitBlock(newNode);
    if (newNode.statements.length === 0) {
      return;
    }
    for (let i = 0; i < newNode.statements.length - 1; ++i) {
      switch ((newNode.statements[i].visit(this._returnChecker) as string) ?? '') {
        case this._returnChecker.returnStyle.definitelyReturns:
        case this._returnChecker.returnStyle.definitelyDoesntReturn:
          throw new WTypeError(newNode.statements[i + 1].origin!.originString, 'Unreachable code');
        case this._returnChecker.returnStyle.hasntReturnedYet:
          continue;
        default:
          break;
      }
    }
  }
}

class VariableDecl extends HmValue {
  private _initializer: HmValue | null;

  constructor(origin: LexerToken, name: string, type: Node, initializer: HmValue | null) {
    super();
    this.origin = origin;
    this.name = name;
    this.hmType = type as HmType;
    this._initializer = initializer;
  }

  get initializer(): HmValue | null {
    return this._initializer;
  }

  get varIsLValue(): boolean {
    return true;
  }

  toString(): string {
    return `${this.hmType!.toString() ?? ''} ` + this.name + (this.initializer != null ? ` = ${this.initializer.toString() ?? ''}` : '');
  }
}

class VariableRef extends Expression {
  variable: HmValue | null;

  constructor(origin: LexerToken, name: string) {
    super(origin);
    this.name = name;
    this.variable = null;
    this._addressSpace = 'thread';
  }

  static wrap(variable: HmValue): VariableRef {
    let result = new VariableRef(variable.origin!, variable.name);
    result.variable = variable;
    return result;
  }

  get isConstexpr(): boolean {
    {
      if (this.isBecome) {
        return this.target.isConstexpr;
      } else {
        return this.variable!.isConstexpr;
      }
    }
  }

  get unifyNode(): Node {
    // This only makes sense when this is a constexpr.
    {
      if (this.isBecome) {
        return this.target.unifyNode;
      } else {
        return this.variable!.unifyNode as ConstexprTypeParameter;
      }
    }
  }

  get isLValue(): boolean {
    {
      if (this.isBecome) {
        return this.target.isLValue;
      } else {
        if (this.variable instanceof FuncParameter) {
          return (this.variable as FuncParameter).varIsLValue;
        }
        if (this.variable instanceof VariableDecl) {
          return (this.variable as VariableDecl).varIsLValue;
        }
        return false;
      }
    }
  }

  get addressSpace(): string {
    {
      if (this.isBecome) {
        return this.target.addressSpace!;
      } else {
        return 'thread';
      }
    }
  }

  toString(): string {
    if (this.isBecome) {
      return `(${this.target.toString()})`;
    } else {
      return this.name;
    }
  }
}
class VisitingSet {
  private _set: Set<Node>;

  constructor(items: Node[] = []) {
    this._set = new Set(items);
  }

  doVisit<T>(item: Node, callback: () => T): T {
    if (this._set.has(item)) {
      throw new WTypeError(item.origin!.originString, `Recursive ${item.kind}`);
    }
    this._set.add(item);
    try {
      return callback();
    } finally {
      this._set.delete(item);
    }
  }
}

class NormalError extends Error {
  message: string;
  constructor(message: string) {
    super();
    this.message = message;
  }
}

class WSyntaxError extends NormalError {
  originString: string;
  syntaxErrorMessage: string;

  constructor(originString: string, message: string) {
    super(`Syntax error at ${originString}: ${message}`);
    this.originString = originString;
    this.syntaxErrorMessage = message;
  }
}
class WTrapError extends NormalError {
  originString: string;
  syntaxErrorMessage: string;

  constructor(originString: string, message: string) {
    super(`Trap at ${originString}: ${message}`);
    this.originString = originString;
    this.syntaxErrorMessage = message;
  }
}

class WTypeError extends NormalError {
  originString: string;
  typeErrorMessage: string;

  constructor(originString: string, message: string = '') {
    super(`Type error at ${originString}: ${message}`);
    this.originString = originString;
    this.typeErrorMessage = message;
  }
}

class WrapChecker extends Visitor {
  private _startNode: Node;
  constructor(node: Node) {
    super();
    this._startNode = node;
  }

  visitVariableRef(node: Node): void {}

  visitTypeRef(node: Node): void {}

  originString(node: Node): string {
    let origin: LexerToken | null = node.origin;
    if (origin == null) {
      return '<null origin>';
    }
    return origin!.originString;
  }

  foundUnwrapped(node: Node): void {
    let classStr = node.constructor.name;
    let classStr1 = this._startNode.constructor.name;
    throw new Error(
      `Found unwrapped ${classStr} at ${this.originString(node)}: ${node.toString()}\nWhile visiting ${classStr1} at ${this.originString(
        this._startNode
      )}: ${this._startNode.toString()}`
    );
  }

  visitConstexprTypeParameter(node: Node): void {
    this.foundUnwrapped(node);
  }

  visitFuncParameter(node: Node): void {
    this.foundUnwrapped(node);
  }

  visitVariableDecl(node: Node): void {
    this.foundUnwrapped(node);
  }

  visitStructType(node: Node): void {
    this.foundUnwrapped(node);
  }

  visitNativeType(node: Node): void {
    this.foundUnwrapped(node);
  }

  visitTypeVariable(node: Node): void {
    this.foundUnwrapped(node);
  }

  // NOTE: This does not know how to handle NativeTypeInstance, because this is never called on instantiated
  // code. Once code is instantiated, you cannot instantiate it further.

  // NOTE: This needs to be kept in sync with AutoWrapper.
}

class GenericLiteralType extends HmType {
  private _value: number;
  preferredType: TypeRef;
  _config: ConfigType;

  constructor(origin: LexerToken, value: number, config: ConfigType) {
    super();
    this._value = value;
    this._config = config;
    this.preferredType = new TypeRef(origin, config.preferredTypeName, []);
    this.origin = origin;
  }
  get value(): number {
    return this._value;
  }

  get isUnifiable(): boolean {
    return true;
  }

  get isLiteral(): boolean {
    return true;
  }

  typeVariableUnify(unificationContext: UnificationContext, other: Node): boolean {
    if (other instanceof HmType === false) {
      return false;
    }
    return this.typeVariableUnifyImpl(unificationContext, other);
  }

  unifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    return this.typeVariableUnify(unificationContext, other as HmType);
  }

  prepareToVerify(unificationContext: UnificationContext): (() => VerifyResult) | null {
    let realThis: Node = unificationContext.find(this);
    if (realThis instanceof TypeVariable || realThis.isLiteral) {
      return (): VerifyResult => {
        if (realThis.unify(unificationContext, this.preferredType)) {
          return new VerifyResult(true);
        }
        return new VerifyResult(false, `Type mismatch between ${unificationContext.find(realThis)} and ${this.preferredType}`);
      };
    }
    return null;
  }

  verifyAsArgument(unificationContext: UnificationContext): VerifyResult {
    return this._config.verifyAsArgument(unificationContext, this);
  }

  verifyAsParameter(unificationContext: UnificationContext): VerifyResult {
    throw new Error('GenericLiteralType should never be used as a type parameter');
  }

  conversionCost(unificationContext: UnificationContext): number {
    let realThis: NativeType = unificationContext.find(this) as NativeType;
    if (realThis.equals(this.preferredType) != null) {
      return 0;
    }
    return 1;
  }

  commitUnification(unificationContext: UnificationContext): void {
    this.hmType = unificationContext.find(this).visit(new AutoWrapper()) as TypeRef | null;
  }

  toString(): string {
    return `${this._config.preferredTypeName}LiteralType<${this.value}>`;
  }
}
class LexerTokenExternal extends LexerToken {
  constructor(originString: string, originKind: string) {
    let lexer: Lexer = new Lexer(originString, originKind, 0, '');
    super(lexer, 0, '', '');
  }
}
class GenericLiteral extends Expression {
  literalClassName: string = '';
  preferredTypeName: string = '';
  _config: Config | null;

  constructor(origin: LexerToken, value: number, config: Config | null = null) {
    super(origin);
    this._value = value;
    this._config = config;
    this.origin = origin;
    if (config != null) {
      this.hmType = config!.createType(origin, value);
    }
  }

  static withType(origin: LexerToken, value: number, type: NativeType): GenericLiteral {
    let result: GenericLiteral = new GenericLiteral(origin, value);
    result.hmType = TypeRef.wrap(type);
    return result;
  }

  get value(): number {
    return this._value as number;
  }
  // This is necessary because once we support int64, we'll need that to be represented as an object
  // rather than as a primitive. Then we'll need to convert.
  get valueForSelectedType(): number {
    {
      let type = this.hmType!.hmType!.unifyNode;
      if (type == null) {
        throw new Error(`Cannot get type for ${this}`);
      }
      let func1: (value: number) => number = (type! as NativeType).formatValueFrom;
      if (func1 == null) {
        throw new Error(`Cannot get function to format type for ${this.literalClassName} from ${type.toString()}`);
      }
      return func1!(this.value as number);
    }
  }

  get isConstexpr(): boolean {
    return true;
  }

  get isLiteral(): boolean {
    return true;
  }

  get negConstexpr(): (() => GenericLiteral) | null {
    {
      if (this._config == null || this._config!.negConstexpr == null) {
        return null;
      }
      return () => this._config!.negConstexpr(this.origin!, this.value as number);
    }
  }

  unifyImpl(unificationContext: UnificationContext, other: Node): boolean {
    if (!(other instanceof GenericLiteral)) {
      return false;
    }
    return (this.value as number) === ((other as GenericLiteral).value as number);
  }

  toString(): string {
    return `${this._config?.preferredTypeName ?? 'null'}Literal<${this.value}>`;
  }
}

class HmSymbol {}

class WslSymbol extends Error {}

class Config {
  literalClassName: string;
  preferredTypeName: string;
  negConstexpr: (origin: LexerToken, value: number) => GenericLiteral;
  createType: (origin: LexerToken, value: number) => GenericLiteralType;
  constructor(
    literalClassName: string,
    preferredTypeName: string,
    negConstexpr: (origin: LexerToken, value: number) => GenericLiteral,
    createType: (origin: LexerToken, value: number) => GenericLiteralType
  ) {
    this.literalClassName = literalClassName;
    this.preferredTypeName = preferredTypeName;
    this.negConstexpr = negConstexpr;
    this.createType = createType;
  }
}

class ConfigType {
  preferredTypeName: string;
  verifyAsArgument: (unificationContext: UnificationContext, type: GenericLiteralType) => VerifyResult;
  constructor(preferredTypeName: string, verifyAsArgument: (unificationContext: UnificationContext, type: GenericLiteralType) => VerifyResult) {
    this.preferredTypeName = preferredTypeName;
    this.verifyAsArgument = verifyAsArgument;
  }
}

let anonymousVariableCount = 0;
let eBufferCount = 0;
let canAllocateEBuffers = true;
let externalOrigin: LexerToken = new LexerTokenExternal('<external>', 'user');
let breakException = 'BreakException';
let continueException = 'ContinueException';
let addressSpaces = ['constant', 'device', 'threadgroup', 'thread'];

function isAddressSpace(addressSpace: string): boolean {
  switch (addressSpace) {
    case 'constant':
      return true;
    case 'device':
      return true;
    case 'threadgroup':
      return true;
    case 'thread':
      return true;
    default:
      return false;
  }
}
function validateAddressSpace(addressSpace: string): void {
  if (!isAddressSpace(addressSpace)) {
    throw new Error(`Bad address space: ${addressSpace}`);
  }
}

class DoubleLiteral extends GenericLiteral {
  constructor(origin: LexerToken, value: number) {
    super(origin, value, doubleConfig);
  }
}
class DoubleLiteralType extends GenericLiteralType {
  constructor(origin: LexerToken, value: number) {
    super(origin, value, doubleConfigType);
  }
}

class FloatLiteral extends GenericLiteral {
  constructor(origin: LexerToken, value: number) {
    super(origin, value, floatConfig);
  }
}

class FloatLiteralType extends GenericLiteralType {
  constructor(origin: LexerToken, value: number) {
    super(origin, value, floatConfigType);
  }
}

function foldConstexprs(program: Program): void {
  program.visit(new ConstexprFolder());
}

function findHighZombies(program: Program): void {
  program.visit(new HighZombieFinder());
}

function flattenProtocolExtends(program: Program): void {
  let visiting: VisitingSet = new VisitingSet();
  for (let protocol1 of program.protocols.values()) {
    flatten(protocol1 as ProtocolDecl, visiting);
  }
}
let anything = new HmSymbol();
function isWildcardKind(kind: Function | HmSymbol): boolean {
  if (kind instanceof HmSymbol) {
    return true;
  }
  return false;
}
function isOriginKind(originKind: string): boolean {
  switch (originKind) {
    case 'native':
    case 'user':
      return true;
    default:
      return false;
  }
}

function parse(program: Program, origin: string, originKind: string, lineNumberOffset: number, text: string): void {
  lexer = new Lexer(origin, originKind, lineNumberOffset, text);
  hmOrigin = origin;
  // The hardest part of dealing with C-like languages is parsing variable declaration statements.
  // Let's consider if this happens in WSL. Here are the valid statements in WSL that being with an
  // identifier, if we assume that any expression can be a standalone statement.
  //
  //     x;
  //     x <binop> y;
  //     x < y;
  //     x < y > z;
  //     x = y;
  //     x.f = y;
  //     \exp = y;
  //     x[42] = y;
  //     x();
  //     x<y>();
  //     x y;
  //     x<y> z;
  //     device x[] y;
  //     x[42] y;
  //     device x^ y;
  //     thread x^^ y;
  //     x^thread^thread y;
  //     x^device^thread y;
  //
  // This has two problem areas:
  //
  //     - x<y>z can parse two different ways (as (x < y) > z or x<y> z).
  //     - x[42] could become either an assignment or a variable declaration.
  //     - x<y> could become either an assignment or a variable declaration.
  //
  // We solve the first problem by forbidding expressions as statements. The lack of function
  // pointers means that we can still allow function call statements - unlike in C, those cannot
  // have arbitrary expressions as the callee. The remaining two problems are solved by
  // backtracking. In all other respects, this is a simple recursive descent parser.

  while (true) {
    let token: null | LexerToken = lexer.peek();
    if (token == null) {
      return;
    }
    if (token!.text === ';') {
      lexer!.next();
    } else if (token!.text === 'typedef') {
      program.add(parseTypeDef());
    } else if (originKind === 'native' && token!.text === 'native') {
      program.add(parseNative());
    } else if (originKind === 'native' && token!.text === 'restricted') {
      program.add(parseRestrictedFuncDef());
    } else if (token!.text === 'struct') {
      program.add(parseStructType());
    } else if (token!.text === 'enum') {
      program.add(parseEnumType());
    } else if (token!.text === 'protocol') {
      program.add(parseProtocolDecl());
    } else {
      program.add(parseFuncDef());
    }
  }
}
function programWithUnnecessaryThingsRemoved(program: Program): Program {
  let nameFinder: NameFinder = new NameFinder();

  // Build our roots.
  for (let statement of program.topLevelStatements as FuncDef[]) {
    if (statement.origin!.originKind === 'user') {
      nameFinder.add(statement.name);
    }
  }

  // Unfortunately, we cannot know yet which operator casts we'll need.
  nameFinder.add('operator cast');

  // We need these even if the program doesn't mention them by name.
  nameFinder.add('void');
  nameFinder.add('bool');
  nameFinder.add('int');

  // Pull in things as necessary.
  while (nameFinder.workList.length > 0) {
    let name = nameFinder.workList.pop();
    let tmp = program.globalNameContext.underlyingThings(anything, name!);
    for (let thing of tmp) {
      thing.visit(nameFinder);
    }
  }

  let result: Program = new Program();
  for (let name of nameFinder.set) {
    for (let thing of program.globalNameContext.underlyingThings(anything, name)) {
      result.add(thing);
    }
  }

  return result;
}
class UintLiteral extends GenericLiteral {
  constructor(origin: LexerToken, value: number) {
    super(origin, value, uintConfig);
  }
}

// This allows you to pass structs and arrays in-place, but it's a more annoying API.
function callFunction(program: Program, name: string, typeArguments: Node[], argumentList: TypedValue[]): TypedValue {
  let argumentTypes = argumentList.map(($0) => $0.hmType);
  let funcOrFailures = resolveInlinedFunction(program, name, typeArguments, argumentTypes, true);
  if (!(funcOrFailures instanceof Func)) {
    let failures: OverloadResolutionFailure[] = funcOrFailures as OverloadResolutionFailure[];
    let arr = failures.map((failure: OverloadResolutionFailure): string => {
      return failure.toString();
    });
    throw new WTypeError(
      `<callFunction>' + 'Cannot resolve function call ${name}<${typeArguments}>(${argumentList})` + (failures.length ? '; tried:\n' + arr.join('\n') : '')
    );
  }
  let func1: Func = funcOrFailures as Func;
  for (let i = 0; i < func1.parameters.length; i++) {
    let type = argumentTypes[i].instantiatedType;
    type.visit(new StructLayoutBuilder());
    func1.parameters[i].ePtr!.copyFrom(argumentList[i].ePtr!, type.size!);
  }
  let result: EPtr | null = new Evaluator(program).runFunc(func1 as FuncDef);
  return new TypedValue(func1.uninstantiatedReturnType, result);
}

function check(program: Program): void {
  program.visit(new Checker(program));
}

function checkLiteralTypes(program: Program): void {
  program.visit(new LiteralTypeChecker());
}

function checkLoops(program: Program): void {
  program.visit(new LoopChecker());
}

function checkRecursion(program: Program): void {
  program.visit(new RecursionChecker());
}

function checkRecursiveTypes(program: Program): void {
  program.visit(new RecursiveTypeChecker());
}

function checkReturns(program: Program): void {
  program.visit(new ReturnChecker(program));
}

function checkUnreachableCode(program: Program): void {
  program.visit(new UnreachableCodeChecker(program));
}

function checkExpressionWrapped(node: Node): void {
  node.visit(new WrapChecker(node));
}

function checkProgramWrapped(node: Node): void {
  node.visit(new ExpressionFinder(checkExpressionWrapped));
}

function cloneProgram(program: Program): Program {
  let result: Program = new Program();
  let cloner: StatementCloner = new StatementCloner();
  for (let statement of program.topLevelStatements) {
    result.add(statement.visit(cloner) as Node);
  }
  return result;
}

const doubleConfig: Config = new Config(
  'DoubleLiteral',
  'double',
  (origin: LexerToken, value: number): IntLiteral => {
    return new IntLiteral(origin, -1 * value);
  },
  (origin: LexerToken, value: number) => {
    return new DoubleLiteralType(origin, value);
  }
);

const doubleConfigType: ConfigType = new ConfigType('double', (unificationContext: UnificationContext, type: GenericLiteralType): VerifyResult => {
  let realThis = unificationContext.find(type) as HmType;
  if (!realThis.isFloating) {
    return new VerifyResult(false, `Cannot use double literal with non-floating type ${realThis}`);
  }
  if (!realThis.canRepresent(type.value)) {
    return new VerifyResult(false, `Float literal ${type.value} does not fit in type ${realThis}`);
  }
  return new VerifyResult(true);
});

class VerifyResult {
  result: boolean = false;
  reason: string = '';

  constructor(result: boolean, reason: string = '') {
    this.result = result;
    this.reason = reason;
  }
}

function flatten(protocol1: ProtocolDecl, visiting: VisitingSet): void {
  if (protocol1.extends.length === 0) {
    return;
  }

  visiting.doVisit(protocol1, () => {
    for (let rawParent of protocol1.extends as ProtocolRef[]) {
      const parent: ProtocolDecl = rawParent.protocolDecl!;
      flatten(parent!, visiting);
      for (let signature of parent!.signatures as ProtocolFuncDecl[]) {
        let newSignature = signature.visit(new Substitution([parent!.typeVariable], [protocol1.typeVariable]));
        protocol1.add(newSignature as ProtocolFuncDecl);
      }
    }
    protocol1.extends = [];
  });
}

const floatConfig: Config = new Config(
  'FloatLiteral',
  'float',
  (origin: LexerToken, value: number): IntLiteral => {
    return new IntLiteral(origin, -1 * value);
  },
  (origin: LexerToken, value: number): FloatLiteralType => {
    return new FloatLiteralType(origin, value);
  }
);

const floatConfigType: ConfigType = new ConfigType('float', (unificationContext: UnificationContext, type: GenericLiteralType): VerifyResult => {
  let realThis = unificationContext.find(type) as HmType;
  if (!realThis.isFloating) {
    return new VerifyResult(false, `Cannot use float literal with non-floating type ${realThis}`);
  }
  if (!realThis.canRepresent(type.value)) {
    return new VerifyResult(false, `Float literal ${type.value} does not fit in type ${realThis}`);
  }
  return new VerifyResult(true);
});

class ResultingFunc {
  func1: Func;
  typeArguments: Array<Node>;

  constructor(func1: Func, typeArguments: Array<Node>) {
    this.func1 = func1;
    this.typeArguments = typeArguments;
  }
}

class FindTypeVariable extends Visitor {
  private func1: Func | null;
  private typeArguments: Array<Node>;

  constructor(func1: Func | null, typeArguments: Array<Node>) {
    super();
    this.func1 = func1;
    this.typeArguments = typeArguments;
  }

  visitTypeRef(node: Node): void {
    for (let typeArgument of (node as TypeRef).typeArguments) {
      typeArgument.visit(this);
    }
  }

  visitTypeVariable(node: Node): void {
    throw new Error(`Unexpected type variable: ${node.toString()} when instantiating ${this.func1?.toString()} with arguments ${this.typeArguments}`);
  }
}

class InstantiationSubstitution extends Substitution {
  thisInstantiator: FuncInstantiator;

  constructor(thisInstantiator: FuncInstantiator, parameters: Array<Node>, argumentList: Array<Node>) {
    super(parameters, argumentList);
    this.thisInstantiator = thisInstantiator;
  }

  visitCallExpression(node: Node): CallExpression {
    let result = super.visitCallExpression(node)!;

    // We may have to re-resolve the function call, if it was a call to a protocol
    // signature.
    if (result.func1 instanceof ProtocolFuncDecl) {
      let overload: ResolveOverloadResult = resolveOverloadImpl(result.possibleOverloads!, result.typeArguments, result.argumentTypes, result.returnType);
      if (overload.func1 == null) {
        let arr = overload.failures!.map((failure: OverloadResolutionFailure): string => {
          return failure.toString();
        });
        let failureStr = arr.join('\n');
        throw new Error(
          `Could not resolve protocol signature function call during instantiation: ${result.func1?.toString()}` +
            (overload.failures!.length !== 0 ? '; tried:\n' + failureStr : '')
        );
      }
      (result as CallExpression).resolveToOverload(overload);
    }
    if (result.func1!.isNative === true) {
      result.nativeFuncInstance = this.thisInstantiator.getUnique(result.func1 as Func, result.actualTypeArguments!);
    }
    return result;
  }
}

class Instantiate extends Base {
  substitution: InstantiationSubstitution;
  instantiateImmediates: InstantiationInstantiateImmediates;

  constructor(substitution: InstantiationSubstitution, instantiateImmediates: InstantiationInstantiateImmediates) {
    super();
    this.substitution = substitution;
    this.instantiateImmediates = instantiateImmediates;
  }

  visitFuncDef(func1: Node): FuncDef {
    let newFunc = func1 as FuncDef;
    let returnType: HmType = newFunc.returnType.visit(this.substitution) as HmType;
    returnType = returnType.visit(this.instantiateImmediates) as HmType;
    let parameters: FuncParameter[] = newFunc.parameters.map(($0: FuncParameter) => $0.visit(this.substitution) as FuncParameter);
    parameters = parameters.map(($0: FuncParameter) => $0.visit(this.instantiateImmediates) as FuncParameter);
    let body: Block = newFunc.body.visit(this.substitution) as Block;
    body = body.visit(this.instantiateImmediates) as Block;
    return new FuncDef(newFunc.origin!, newFunc.name, returnType, [], parameters, body, newFunc.isCast, newFunc.shaderType);
  }

  visitNativeFunc(func1: Node): NativeFuncInstance {
    let newFunc = func1 as NativeFunc;
    return new NativeFuncInstance(
      newFunc,
      (newFunc.returnType.visit(this.substitution) as HmType).visit(this.instantiateImmediates) as HmType,
      newFunc.parameters.map(
        (parameter: FuncParameter): FuncParameter => (parameter.visit(this.substitution) as FuncParameter).visit(this.instantiateImmediates) as FuncParameter
      ),
      newFunc.isCast,
      newFunc.shaderType,
      newFunc.instantiateImplementation(this.substitution)
    );
  }
  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    let methods = new Map<string, (node: Base) => void | Base | string | null>();
    methods.set('visitFuncDef', this.visitFuncDef as (node: Base) => FuncDef);
    methods.set('visitNativeFunc', this.visitNativeFunc as (node: Base) => NativeFuncInstance);
    return methods;
  }
}

// If a high-level construct somehow manages to live on when we're lowered, it's a high zombie.
function inferTypesForCall(
  func1: Func,
  typeArguments: Node[],
  argumentTypes: Node[],
  returnType: TypeRef | null
): ResolveOverloadResult | OverloadResolutionSuccess {
  if (typeArguments.length > 0 && typeArguments.length !== func1.typeParameters.length) {
    return new ResolveOverloadResult(
      new OverloadResolutionFailure(func1, `Wrong number of type arguments (passed ${typeArguments.length}, require ${func1.typeParameters.length})`),
      null,
      null,
      null,
      null,
      null
    );
  }
  if (argumentTypes.length !== func1.parameters.length) {
    return new ResolveOverloadResult(
      new OverloadResolutionFailure(func1, `Wrong number of arguments (passed ${argumentTypes.length}, require ${func1.parameters.length})`),
      null,
      null,
      null,
      null,
      null
    );
  }
  let unificationContext: UnificationContext = new UnificationContext(func1.typeParametersForCallResolution);
  for (let i = 0; i < typeArguments.length; ++i) {
    let argument = typeArguments[i];
    let parameter: Node = func1.typeParameters[i];
    if (!argument.unify(unificationContext, parameter)) {
      return new ResolveOverloadResult(
        new OverloadResolutionFailure(
          func1,
          `Type argument #${i + 1} for parameter ${parameter.name} does not match (passed ${argument}, require ${parameter})`
        ),
        null,
        null,
        null,
        null,
        null
      );
    }
  }
  for (let i = 0; i < argumentTypes.length; ++i) {
    if (argumentTypes[i] == null) {
      throw new Error(`Null argument type at i = ${i}`);
    }
    if (!argumentTypes[i].unify(unificationContext, func1.parameters[i].hmType!)) {
      return new ResolveOverloadResult(
        new OverloadResolutionFailure(
          func1,
          `Argument #${i + 1} ` +
            (func1!.parameters[i].name ? `for parameter ${func1!.parameters[i].name} ` : '') +
            `does not match (passed ${argumentTypes[i]}, require ${func1!.parameters[i].hmType})`
        ),
        null,
        null,
        null,
        null,
        null
      );
    }
  }
  if (returnType != null && !returnType!.unify(unificationContext, func1.returnType)) {
    return new ResolveOverloadResult(
      new OverloadResolutionFailure(func1, `Return type ${func1!.returnType} does not match ${returnType}`),
      null,
      null,
      null,
      null,
      null
    );
  }
  let verificationResult: VerifyResult = unificationContext.verify();
  if (!verificationResult.result) {
    return new ResolveOverloadResult(new OverloadResolutionFailure(func1!, verificationResult.reason), null, null, null, null, null);
  }
  let shouldBuildTypeArguments = typeArguments.length === 0;
  let newTypeArguments: Node[] = typeArguments;
  if (shouldBuildTypeArguments) {
    newTypeArguments = [];
  }
  for (let typeParameter of func1.typeParameters) {
    let typeArgument: Node = unificationContext.find(typeParameter);
    if (typeArgument === typeParameter) {
      return new ResolveOverloadResult(
        new OverloadResolutionFailure(func1, `Type parameter ${typeParameter.name} did not get assigned a type`),
        null,
        null,
        null,
        null,
        null
      );
    }
    if (shouldBuildTypeArguments) {
      newTypeArguments.push(typeArgument);
    }
  }
  return new OverloadResolutionSuccess(func1, unificationContext, newTypeArguments);
}

function inline(program: Program): void {
  for (let funcList of program.functions.values()) {
    for (let func1 of funcList as FuncDef[]) {
      if (func1.typeParameters.length === 0) {
        const func2 = program.funcInstantiator.getUnique(func1, []);
        inlineFunction(program, func2, new VisitingSet([func2]));
      }
    }
  }
}

function inlineFunction(program: Program, func1: Func, visiting: VisitingSet): void {
  if (func1.typeParameters.length > 0) {
    throw new Error('Cannot inline func1tion that has type parameters');
  }
  if (func1.inlined || func1.isNative) {
    return;
  }
  let funcDef = func1 as FuncDef;
  funcDef.visit(new LateChecker());

  // This is the precise time when we can build EBuffers in order to get them to be uniqued by
  // type instantiation but nothing else.
  funcDef.visit(new StructLayoutBuilder());
  funcDef.visit(new EBufferBuilder(program));

  funcDef.rewrite(new Inliner(program, funcDef, visiting));

  funcDef.inlined = true;
}

function resolveInlinedFunction(
  program: Program,
  name: string,
  typeArguments: Node[],
  argumentTypes: HmType[],
  allowEntryPoint = false
): FuncDef | [] | OverloadResolutionFailure[] | Func {
  let overload: ResolveOverloadResult = program.globalNameContext.resolveFuncOverload(name, typeArguments, argumentTypes, null, allowEntryPoint);
  if (overload.func1 == null) {
    return overload.failures as OverloadResolutionFailure[];
  }
  if (overload.func1.typeParameters == null) {
    return overload.func1;
  }
  let func1 = program.funcInstantiator.getUnique(overload.func1!, overload.typeArguments!) as FuncDef;
  inlineFunction(program, func1, new VisitingSet([overload.func1 as FuncDef]));
  return func1;
}

class StateResult {
  index: number;
  stack: (LexerToken | null)[];

  constructor(index: number, stack: (LexerToken | null)[]) {
    this.index = index;
    this.stack = stack;
  }
}
class KindArray {
  array: Func[] = [];
  kind: string | null = null;
}
class WhileLoop extends Node {
  private _conditional: HmValue;
  constructor(origin: LexerToken, conditional: HmValue, body: Node) {
    super();
    this._conditional = conditional;
    this.body = body;
    this.origin = origin;
  }

  get conditional(): HmValue {
    return this._conditional;
  }
  toString(): string {
    return `while (${this.conditional.toString()}) ${this.body.toString()}`;
  }
}
class ImplementationDataType {
  hmType: StructType;
  fieldName: string = '';
  offset: number | null = null;
  structSize: number | null = null;
  fieldSize: number | null = null;

  constructor(hmType: StructType, fieldName: string) {
    this.hmType = hmType;
    this.fieldName = fieldName;
  }
}
class ResultMemberInfo {
  value: number;
  name: string;

  constructor(value: number, name: string) {
    this.value = value;
    this.name = name;
  }
}
class CallResolveResult {
  call: CallExpression;
  resultType: HmType;

  constructor(call: CallExpression, resultType: HmType) {
    this.call = call;
    this.resultType = resultType;
  }
}
class InstantiationInstantiateImmediates extends InstantiateImmediates {
  visitCallExpression(node: Node): CallExpression {
    // We need to preserve certain things that would have instantiated, but that we cannot
    // instantiate without breaking chain-instantiations (generic function calls generic
    // function so therefore the instantiated generic function must still have the original
    // (uninstantiated) types to instantiate the generic function that it calls).
    // Array<VariableRef|MakePtrExpression|MakeArrayRefExpression|GenericLiteral|DereferenceExpression|DotExpression>
    let newNode = node as CallExpression;
    let result: CallExpression = new CallExpression(
      newNode.origin!,
      newNode.name,
      newNode.typeArguments,
      newNode.argumentList.map((argument: Node): Expression => {
        return Node.visit(argument, this) as Expression;
      })
    );
    result = this.processDerivedCallData(newNode, result)!;

    result.argumentTypes = Array.from(newNode.argumentTypes);
    if (newNode.isCast) {
      result.setCastData(newNode.returnType!);
    }
    result.actualTypeArguments = Array.from(newNode.actualTypeArguments);

    return result;
  }
}

const intConfigType: ConfigType = new ConfigType('int', (unificationContext: UnificationContext, type: GenericLiteralType): VerifyResult => {
  let realThis = unificationContext.find(type) as HmType;
  if (!realThis.isNumber) {
    return new VerifyResult(false, `Cannot use int literal with non-number type ${realThis}`);
  }
  if (!realThis.canRepresent(type.value)) {
    return new VerifyResult(false, `Int literal ${type.value} too large to be represented by type ${realThis}`);
  }
  return new VerifyResult(true);
});

class IntLiteralType extends GenericLiteralType {
  constructor(origin: LexerToken, value: number) {
    super(origin, value, intConfigType);
  }
}

const intConfig: Config = new Config(
  'IntLiteral',
  'int',
  (origin: LexerToken, value: number): IntLiteral => {
    return new IntLiteral(origin, (-1 * value) | 0);
  },
  (origin: LexerToken, value: number): IntLiteralType => {
    return new IntLiteralType(origin, value);
  }
);

class IntLiteral extends GenericLiteral {
  constructor(origin: LexerToken, value: number) {
    super(origin, value, intConfig);
  }
}

let lexer: Lexer | null;
let hmOrigin: string = '';

function genericConsume(callback: (token: LexerToken) => boolean, explanation: string[]): LexerToken {
  let token: null | LexerToken = lexer!.next();
  if (token == null) {
    lexer!.fail('Unexpected end of file');
  }
  if (!callback(token!)) {
    lexer!.fail(`Unexpected token: ${token!.text}; expected: ${explanation.join(',')}`);
  }
  return token!;
}

function consume(texts: string[]): LexerToken {
  return genericConsume((token: LexerToken) => {
    return texts.includes(token.text);
  }, texts);
}

function consumeKind(kind: string): LexerToken {
  return genericConsume(
    (token: LexerToken) => {
      return token.kind === kind;
    },
    [kind]
  );
}

function assertNext(texts: string[]): void {
  lexer!.push(consume(texts));
}

function genericTest(callback: (token: LexerToken) => boolean): LexerToken | null {
  let token: null | LexerToken = lexer!.peek();
  if (token != null && callback(token)) {
    return token;
  }
  return null;
}

function test(texts: string[]): LexerToken | null {
  return genericTest((token: LexerToken) => texts.includes(token.text));
}

function testKind(kind: string): LexerToken | null {
  return genericTest((token: LexerToken) => token.kind === kind);
}

function tryConsume(texts: string[]): LexerToken {
  let result = test(texts);
  if (result != null) {
    lexer!.next();
  }
  return result!;
}

function tryConsumeKind(kind: string): LexerToken | null {
  let result = testKind(kind);
  if (result != null) {
    lexer!.next();
  }
  return result;
}

function parseProtocolRef(): ProtocolRef {
  let protocolToken = consumeKind('identifier');
  return new ProtocolRef(protocolToken, protocolToken.text);
}

function consumeEndOfTypeArgs(): void {
  let rightShift = tryConsume(['>>']);
  if (rightShift != null) {
    lexer!.push(new LexerToken(lexer!, rightShift.index!, rightShift.kind, '>'));
  } else {
    consume(['>']);
  }
}

function parseTypeParameters(): Node[] {
  if (test(['<']) == null) {
    return [];
  }

  let result: Node[] = [];
  consume(['<']);
  while (test(['>']) == null) {
    let constexpr = lexer!.backtrackingScope(() => {
      let type = parseType();
      let name = consumeKind('identifier');
      assertNext([',', '>', '>>']);
      return new ConstexprTypeParameter(type.origin!, name.text, type as TypeRef);
    });
    if (constexpr != null) {
      result.push(constexpr);
    } else {
      let name = consumeKind('identifier');
      let hmProtocol = tryConsume([':']) != null ? parseProtocolRef() : null;
      result.push(new TypeVariable(name, name.text, hmProtocol));
    }
    if (tryConsume([',']) == null) {
      break;
    }
  }
  consumeEndOfTypeArgs();
  return result;
}

function parseTerm(): Expression {
  let token: LexerToken | null;
  token = tryConsume(['null']);
  if (token != null) {
    return new NullLiteral(token);
  }
  token = tryConsumeKind('identifier');
  if (token != null) {
    return new VariableRef(token, token.text);
  }
  token = tryConsumeKind('intLiteral');
  if (token != null) {
    let intVersion: number = Number(token.text) | 0;
    if (`${intVersion}` !== token.text) {
      lexer!.fail(`Integer literal is not an integer: ${token.text}`);
    }
    if (intVersion > WSL_INT32MAX) {
      lexer!.fail('Expected assignment');
    }
    return new IntLiteral(token, intVersion);
  }
  token = tryConsumeKind('uintLiteral');
  if (token != null) {
    let uintVersion = Number(token.text.substr(0, token.text.length - 1)) >>> 0;
    if (`${uintVersion}u` !== token.text) {
      lexer!.fail(`Integer literal is not 32-bit unsigned integer: ${token.text}`);
    }
    return new UintLiteral(token, uintVersion);
  }
  token = tryConsumeKind('intHexLiteral') ?? tryConsumeKind('uintHexLiteral');
  if (token != null) {
    let hexString = token.text.substr(WSL_2);
    if (token.kind === 'uintHexLiteral') {
      hexString = hexString.substr(0, hexString.length - 1);
    }
    if (hexString.length === 0) {
      throw new Error(`Bad hex literal: ${token}`);
    }
    let intVersion = Number.parseInt(hexString, WSL_16);
    if (token.kind === 'intHexLiteral') {
      intVersion = intVersion | 0;
    } else {
      intVersion = intVersion >>> 0;
    }
    if (intVersion.toString(WSL_16) !== hexString) {
      lexer!.fail(`Hex integer literal is not an integer: ${token.text}`);
    }
    if (token.kind === 'intHexLiteral') {
      return new IntLiteral(token, intVersion);
    }
    return new UintLiteral(token, intVersion >>> 0);
  }
  token = tryConsumeKind('doubleLiteral');
  if (token != null) {
    return new DoubleLiteral(token, Number(token.text));
  }
  token = tryConsumeKind('floatLiteral');
  if (token != null) {
    let text: string = token.text;
    let d = token.text.endsWith('d');
    let f = token.text.endsWith('f');
    if (d && f) {
      throw new Error('Literal cannot be both a double literal and a float literal.');
    }
    if (d || f) {
      text = text.substring(0, text.length - 1);
    }
    let value = Number.parseFloat(text);
    if (d) {
      return new DoubleLiteral(token, value);
    }
    return new FloatLiteral(token, Math.fround(value));
  }
  token = tryConsume(['true', 'false']);
  if (token != null) {
    return new BoolLiteral(token, token.text === 'true');
  }
  // FIXME: Need support for other literals too.
  consume(['(']);
  let result = parseExpression();
  consume([')']);
  return result;
}

function parseConstexpr(): Expression {
  let token: LexerToken | null;
  token = tryConsume(['-']);
  if (token != null) {
    return new CallExpression(token, `operator${token.text}`, [], [parseTerm()]);
  }
  let left = parseTerm();
  token = tryConsume(['.']);
  if (token != null) {
    left = new DotExpression(token, left, consumeKind('identifier').text);
  }
  return left;
}

function parseTypeArguments(): Node[] {
  if (test(['<']) == null) {
    return [];
  }

  let result: Node[] = [];
  consume(['<']);
  while (test(['>']) == null) {
    // It's possible for a constexpr or type can syntactically overlap in the single
    // identifier case. Let's consider the possibilities:
    //
    //     T          could be type or constexpr
    //     T[]        only type
    //     T[42]      only type (constexpr cannot do indexing)
    //     42         only constexpr
    //
    // In the future we'll allow constexprs to do more things, and then we'll still have
    // the problem that something of the form T[1][2][3]... can either be a type or a
    // constexpr, and we can figure out in the checker which it is.
    let typeOrVariableRef = lexer!.backtrackingScope((): TypeOrVariableRef => {
      let result = consumeKind('identifier');
      assertNext([',', '>', '>>']);
      return new TypeOrVariableRef(result, result.text);
    });
    if (typeOrVariableRef != null) {
      result.push(typeOrVariableRef);
    } else {
      let constexpr = lexer!.backtrackingScope((): Expression => {
        let result = parseConstexpr();
        assertNext([',', '>', '>>']);
        return result;
      });
      if (constexpr != null) {
        result.push(constexpr);
      } else {
        result.push(parseType());
      }
    }
    if (tryConsume([',']) == null) {
      break;
    }
  }
  consumeEndOfTypeArgs();
  return result;
}

function parseType(): HmType {
  let token: LexerToken | null;
  let addressSpace: string | null = null;
  let addressSpaceConsumed = false;
  token = tryConsume(addressSpaces)!;
  if (token != null) {
    addressSpace = token.text;
  }

  let name = consumeKind('identifier');
  let typeArguments = parseTypeArguments();
  let hmType: HmType = new TypeRef(name, name.text, typeArguments as TypeOrVariableRef[]);

  const getAddressSpace = (): string => {
    addressSpaceConsumed = true;
    if (addressSpace != null) {
      return addressSpace;
    }
    return consume(addressSpaces).text;
  };

  while ((token = tryConsume(['*', '['])!)) {
    if (token.text === '*') {
      hmType = new PtrType(token, getAddressSpace(), hmType);
      continue;
    }
    if (tryConsume([']']) != null) {
      hmType = new ArrayRefType(token, getAddressSpace(), hmType);
      continue;
    }

    hmType = new ArrayType(token, hmType, parseConstexpr());
    consume([']']);
  }

  if (addressSpace != null && !addressSpaceConsumed) {
    lexer!.fail('Address space specified for type that does not need address space');
  }

  return hmType;
}

function parseTypeDef(): Node {
  let origin = consume(['typedef']);
  let name: string = consumeKind('identifier').text;
  let typeParameters = parseTypeParameters();
  consume(['=']);
  let type = parseType();
  consume([';']);
  return new TypeDef(origin, name, typeParameters, type);
}

function genericParseLeft(
  texts: string[],
  nextParser: () => Expression,
  constructor: (token: LexerToken, hmLeft: Expression, hmRight: Expression) => Expression
): Expression {
  let left = nextParser();
  let token: LexerToken;
  while ((token = tryConsume(texts)!)) {
    left = constructor(token, left, nextParser());
  }
  return left;
}

function parseLeftOperatorCall(texts: string[], nextParser: () => Expression): Expression {
  return genericParseLeft(
    texts,
    nextParser,
    (token, hmLeft, hmRight) => new CallExpression(token, `operator${token.text}`, [], [hmLeft, hmRight] as HmValue[])
  );
}

function parseCallExpression(): CallExpression {
  let name = consumeKind('identifier');
  let typeArguments = parseTypeArguments();
  consume(['(']);
  let argumentList: Expression[] = [];
  while (test([')']) == null) {
    let argument = parsePossibleAssignment();
    argumentList.push(argument);
    if (tryConsume([',']) == null) {
      break;
    }
  }
  consume([')']);
  let result = new CallExpression(name, name.text, typeArguments, argumentList);
  return result;
}

function isCallExpression(): boolean {
  return lexer!.testScope(() => {
    consumeKind('identifier');
    parseTypeArguments();
    consume(['(']);
  });
}

function emitIncrement(token: LexerToken, old: Expression, extraArg: Expression | null): CallExpression {
  let args = [old];
  if (extraArg != null) {
    args.push(extraArg);
  }

  let name = `operator${token.text}`;
  let match = new RegExp('=$').exec(name);
  if (match != null) {
    name = name.slice(0, match.index);
  }
  if (name === 'operator') {
    throw new Error(`Invalid name: ${name}`);
  }
  return new CallExpression(token, name, [], args);
}

function finishParsingPostIncrement(token: LexerToken, hmLeft: Expression): Expression {
  let readModifyWrite = new ReadModifyWriteExpression(token, hmLeft);
  readModifyWrite.newValueExp = emitIncrement(token, readModifyWrite.oldValueRef(), null);
  readModifyWrite.resultExp = readModifyWrite.oldValueRef();
  return readModifyWrite;
}
function parseSuffixOperator(hmLeftValue: Expression, acceptableOperators: string[]): Expression {
  let hmLeft = hmLeftValue;
  let token: LexerToken | null;
  while ((token = tryConsume(acceptableOperators))) {
    switch (token.text) {
      case '++':
      case '--':
        return finishParsingPostIncrement(token, hmLeft);
      case '.':
      case '->':
        if (token.text === '->') {
          hmLeft = new DereferenceExpression(token, hmLeft);
        }
        hmLeft = new DotExpression(token, hmLeft, consumeKind('identifier').text);
        break;
      case '[':
        let index = parseExpression();
        consume([']']);
        hmLeft = new IndexExpression(token, hmLeft, index);
        break;
      default:
        throw new Error(`Bad token: ${token}`);
    }
  }
  return hmLeft;
}

function parsePossibleSuffix(): Expression {
  let acceptableOperators = ['++', '--', '.', '->', '['];
  let limitedOperators = ['.', '->', '['];
  let left: Expression;
  if (isCallExpression()) {
    left = parseCallExpression();
    acceptableOperators = limitedOperators;
  } else {
    left = parseTerm();
  }

  return parseSuffixOperator(left, acceptableOperators);
}

function finishParsingPreIncrement(token: LexerToken, hmLeft: Expression, extraArg: Expression | null): ReadModifyWriteExpression {
  let readModifyWrite = new ReadModifyWriteExpression(token, hmLeft);
  readModifyWrite.newValueExp = emitIncrement(token, readModifyWrite.oldValueRef(), extraArg);
  readModifyWrite.resultExp = readModifyWrite.newValueRef();
  return readModifyWrite;
}

function parsePreIncrement(): ReadModifyWriteExpression {
  let token = consume(['++', '--']);
  let left = parsePossiblePrefix();
  return finishParsingPreIncrement(token, left, null);
}

function parsePossiblePrefix(): Expression {
  let token: LexerToken | null;
  if (test(['++', '--']) != null) {
    return parsePreIncrement();
  }
  token = tryConsume(['+', '-', '~']);
  if (token != null) {
    return new CallExpression(token, `operator${token.text}`, [], [parsePossiblePrefix()] as HmValue[]);
  }
  token = tryConsume(['*']);
  if (token != null) {
    return new DereferenceExpression(token, parsePossiblePrefix());
  }
  token = tryConsume(['&']);
  if (token != null) {
    return new MakePtrExpression(token, parsePossiblePrefix());
  }
  token = tryConsume(['@']);
  if (token != null) {
    return new MakeArrayRefExpression(token, parsePossiblePrefix());
  }
  token = tryConsume(['!']);
  if (token != null) {
    let remainder = parsePossiblePrefix();
    return new LogicalNot(token, new CallExpression(remainder.origin!, 'bool', [], [remainder] as HmValue[]));
  }
  return parsePossibleSuffix();
}
function parsePossibleProduct(): Expression {
  return parseLeftOperatorCall(['*', '/', '%'], parsePossiblePrefix);
}

function parsePossibleSum(): Expression {
  return parseLeftOperatorCall(['+', '-'], parsePossibleProduct);
}

function parsePossibleShift(): Expression {
  return parseLeftOperatorCall(['<<', '>>'], parsePossibleSum);
}

function parsePossibleRelationalInequality(): Expression {
  return parseLeftOperatorCall(['<', '>', '<=', '>='], parsePossibleShift);
}

function parsePossibleRelationalEquality(): Expression {
  return genericParseLeft(['==', '!='], parsePossibleRelationalInequality, (token, hmLeft, hmRight) => {
    let result: Expression = new CallExpression(token, 'operator==', [], [hmLeft, hmRight] as HmValue[]);
    if (token.text === '!=') {
      result = new LogicalNot(token, result);
    }
    return result;
  });
}

function parsePossibleBitwiseAnd(): Expression {
  return parseLeftOperatorCall(['&'], parsePossibleRelationalEquality);
}

function parsePossibleBitwiseXor(): Expression {
  return parseLeftOperatorCall(['^'], parsePossibleBitwiseAnd);
}

function parsePossibleBitwiseOr(): Expression {
  return parseLeftOperatorCall(['|'], parsePossibleBitwiseXor);
}
function parseLeftLogicalExpression(texts: string[], nextParser: () => Expression): Expression {
  return genericParseLeft(texts, nextParser, (token: LexerToken, hmLeft: Expression, hmRight: Expression): Expression => {
    return new LogicalExpression(
      token,
      token.text,
      new CallExpression(hmLeft.origin!, 'bool', [], [hmLeft]),
      new CallExpression(hmRight.origin!, 'bool', [], [hmRight])
    );
  });
}

function parsePossibleLogicalAnd(): Expression {
  return parseLeftLogicalExpression(['&&'], parsePossibleBitwiseOr);
}

function parsePossibleLogicalOr(): Expression {
  return parseLeftLogicalExpression(['||'], parsePossibleLogicalAnd);
}

function parsePossibleTernaryConditional(): Expression {
  let predicate = parsePossibleLogicalOr();
  let _operator = tryConsume(['?']);
  if (_operator == null) {
    return predicate;
  }

  return new TernaryExpression(_operator!, predicate, parsePossibleAssignment(), parsePossibleAssignment());
}
function parsePossibleAssignment(mode: string | null = null): Expression {
  let lhs = parsePossibleTernaryConditional();
  let operator = tryConsume(['=', '+=', '-=', '*=', '/=', '%=', '^=', '|=', '&=']);
  if (operator == null) {
    if (mode === 'required') {
      lexer!.fail('Expected assignment');
    }
    return lhs;
  }
  if (operator!.text === '=') {
    return new Assignment(operator!, lhs, parsePossibleAssignment());
  }
  return finishParsingPreIncrement(operator!, lhs, parsePossibleAssignment());
}

function parseAssignment(): Expression {
  return parsePossibleAssignment('required');
}

function parsePostIncrement(): Expression {
  let left = parseSuffixOperator(parseTerm(), ['.', '->', '[']);
  let token = consume(['++', '--']);
  return finishParsingPostIncrement(token, left);
}

function parseEffectfulExpression(mode: string | null = null): Expression {
  if (isCallExpression()) {
    return parseCallExpression();
  }
  let preIncrement = lexer!.backtrackingScope(parsePreIncrement);
  if (preIncrement != null) {
    return preIncrement! as Expression;
  }
  let postIncrement = lexer!.backtrackingScope(parsePostIncrement);
  if (postIncrement != null) {
    return postIncrement! as Expression;
  }
  return parseAssignment();
}

function genericParseCommaExpression(finalExpressionParser: (mode: string | null) => Expression): Expression {
  let list: Expression[] = [];
  let origin = lexer!.peek();
  if (origin == null) {
    lexer!.fail('Unexpected end of file');
  }
  while (true) {
    let effectfulExpression = lexer!.backtrackingScope((): null => {
      parseEffectfulExpression();
      consume([',']);
      return null;
    });
    if (effectfulExpression == null) {
      let final = finalExpressionParser(null);
      list.push(final);
      break;
    }
    list.push(effectfulExpression as Expression);
  }
  if (list.length === 0) {
    throw new Error('Length should never be zero');
  }
  if (list.length === 1) {
    return list[0];
  }
  return new CommaExpression(origin!, list);
}

function parseCommaExpression(): Expression {
  return genericParseCommaExpression(parsePossibleAssignment);
}

function parseExpression(): Expression {
  return parseCommaExpression();
}

function parseEffectfulStatement(): Expression {
  let result = genericParseCommaExpression(parseEffectfulExpression);
  consume([';']);
  return result;
}

function parseReturn(): Return {
  let origin = consume(['return']);
  if (tryConsume([';']) != null) {
    return new Return(origin, null);
  }
  let expression = parseExpression();
  consume([';']);
  return new Return(origin, expression);
}

function parseBreak(): Break {
  let origin = consume(['break']);
  consume([';']);
  return new Break(origin);
}

function parseContinue(): Continue {
  let origin = consume(['continue']);
  consume([';']);
  return new Continue(origin);
}

function parseIfStatement(): IfStatement {
  let origin = consume(['if']);
  consume(['(']);
  let conditional = parseExpression();
  consume([')']);
  let body = parseStatement();
  let elseBody: Node | null = null;
  if (tryConsume(['else']) != null) {
    elseBody = parseStatement();
  }
  return new IfStatement(origin, new CallExpression(conditional.origin!, 'bool', [], [conditional]), body!, elseBody);
}

function parseWhile(): WhileLoop {
  let origin = consume(['while']);
  consume(['(']);
  let conditional = parseExpression();
  consume([')']);
  let body = parseStatement();
  return new WhileLoop(origin, new CallExpression(conditional.origin!, 'bool', [], [conditional]), body!);
}

function parseFor(): ForLoop {
  let origin = consume(['for']);
  consume(['(']);
  let initialization: Node | null;
  if (tryConsume([';']) != null) {
    initialization = null;
  } else {
    initialization = lexer!.backtrackingScope(parseVariableDecls);
    if (initialization == null) {
      initialization = parseEffectfulStatement();
    }
  }
  let condition: Expression | null;
  let con1 = tryConsume([';']);
  if (con1 != null) {
    condition = null;
  } else {
    condition = parseExpression();
    consume([';']);
    condition = new CallExpression(condition!.origin!, 'bool', [], [condition!]);
  }
  let increment: Expression | null;
  if (tryConsume([')']) != null) {
    increment = null;
  } else {
    increment = parseExpression();
    consume([')']);
  }
  let body: Node | null = parseStatement();
  return new ForLoop(origin, initialization as Expression | null, condition, increment, body!);
}

function parseDo(): DoWhileLoop {
  let origin = consume(['do']);
  let body: Node | null = parseStatement();
  consume(['while']);
  consume(['(']);
  let conditional = parseExpression();
  consume([')']);
  return new DoWhileLoop(origin, body as Block, new CallExpression(conditional.origin!, 'bool', [], [conditional]));
}

function parseVariableDecls(): CommaExpression {
  let type = parseType();
  let list: VariableDecl[] = [];
  do {
    let name = consumeKind('identifier');
    let initializer = tryConsume(['=']) != null ? parseExpression() : null;
    list.push(new VariableDecl(name, name.text, type, initializer));
  } while (consume([',', ';']).text === ',');
  return new CommaExpression(type.origin!, list);
}

function parseSwitchCase(): SwitchCase {
  let token = consume(['default', 'case']);
  let value: Expression | null;
  if (token.text === 'case') {
    value = parseConstexpr();
  }
  consume([':']);
  let body = parseBlockBody(['}', 'default', 'case']);
  return new SwitchCase(token, value!, body);
}

function parseSwitchStatement(): SwitchStatement {
  let origin = consume(['switch']);
  consume(['(']);
  let value = parseExpression();
  consume([')']);
  consume(['{']);
  let result = new SwitchStatement(origin, value);
  while (tryConsume(['}']) == null) {
    result.add(parseSwitchCase());
  }
  return result;
}

function parseStatement(): Node | null {
  let token = lexer!.peek();
  if (token != null && token!.text === ';') {
    lexer!.next();
    return null;
  }
  if (token != null && token!.text === 'return') {
    return parseReturn();
  }
  if (token != null && token!.text === 'break') {
    return parseBreak();
  }
  if (token != null && token!.text === 'continue') {
    return parseContinue();
  }
  if (token != null && token!.text === 'while') {
    return parseWhile();
  }
  if (token != null && token!.text === 'do') {
    return parseDo();
  }
  if (token != null && token!.text === 'for') {
    return parseFor();
  }
  if (token != null && token!.text === 'if') {
    return parseIfStatement();
  }
  if (token != null && token!.text === 'switch') {
    return parseSwitchStatement();
  }
  if (token != null && token!.text === 'trap') {
    let origin = consume(['trap']);
    consume([';']);
    return new TrapStatement(origin);
  }
  if (token != null && token!.text === '{') {
    return parseBlock();
  }
  let variableDecl = lexer!.backtrackingScope(parseVariableDecls);
  if (variableDecl != null) {
    return variableDecl;
  }
  return parseEffectfulStatement();
}

function parseBlockBody(terminators: string[]): Block {
  let block = new Block(hmOrigin);
  while (test(terminators) == null) {
    let statement = parseStatement();
    if (statement != null) {
      block.add(statement!);
    }
  }
  return block;
}

function parseBlock(): Block {
  let origin = consume(['{']);
  let block = parseBlockBody(['}']);
  consume(['}']);
  return block;
}

function parseParameter(): FuncParameter {
  let type = parseType();
  let name = tryConsumeKind('identifier');
  return new FuncParameter(type.origin!, name ? name.text : null, type as HmType);
}

function parseParameters(): FuncParameter[] {
  consume(['(']);
  let parameters: FuncParameter[] = [];
  while (test([')']) == null) {
    parameters.push(parseParameter());
    if (tryConsume([',']) == null) {
      break;
    }
  }
  consume([')']);
  return parameters;
}

function parseFuncName(): string {
  if (tryConsume(['operator']) != null) {
    let token = consume(['+', '-', '*', '/', '%', '^', '&', '|', '<', '>', '<=', '>=', '==', '++', '--', '.', '~', '<<', '>>', '[']);
    if (token.text === '&') {
      if (tryConsume(['[']) != null) {
        consume([']']);
        return 'operator&[]';
      }
      if (tryConsume(['.']) != null) {
        return `operator&.${consumeKind('identifier').text}`;
      }
      return 'operator&';
    }
    if (token.text === '.') {
      let result = `operator.${consumeKind('identifier').text}`;
      if (tryConsume(['=']) != null) {
        result += '=';
      }
      return result;
    }
    if (token.text === '[') {
      consume([']']);
      let result = 'operator[]';
      if (tryConsume(['=']) != null) {
        result += '=';
      }
      return result;
    }
    return `operator${token.text}`;
  }
  return consumeKind('identifier').text;
}

function parseFuncDecl(): Func {
  let origin: LexerToken | null;
  let returnType: HmType;
  let name: string | null;
  let typeParameters: Node[] | null;
  let isCast: boolean | null;
  let shaderType: LexerToken | string | null = null;
  let operatorToken = tryConsume(['operator']);
  if (operatorToken != null) {
    origin = operatorToken;
    typeParameters = parseTypeParameters();
    returnType = parseType();
    name = 'operator cast';
    isCast = true;
  } else {
    shaderType = tryConsume(['vertex', 'fragment'])!;
    returnType = parseType();
    if (shaderType != null) {
      let shaderT = shaderType as LexerToken;
      origin = shaderT;
      shaderType = shaderType.text;
    } else {
      origin = returnType.origin;
    }
    name = parseFuncName();
    typeParameters = parseTypeParameters();
    isCast = false;
  }
  let parameters = parseParameters();
  return new Func(origin!, name!, returnType, typeParameters!, parameters, isCast, shaderType as string);
}

function parseProtocolFuncDecl(): ProtocolFuncDecl {
  let func1 = parseFuncDecl();
  return new ProtocolFuncDecl(func1.origin!, func1.name, func1.returnType, func1.typeParameters, func1.parameters, func1.isCast, func1.shaderType);
}

function parseFuncDef(): FuncDef {
  let func1 = parseFuncDecl();
  let body = parseBlock();
  return new FuncDef(func1.origin!, func1.name, func1.returnType, func1.typeParameters, func1.parameters, body, func1.isCast, func1.shaderType);
}

function parseProtocolDecl(): ProtocolDecl {
  let origin = consume(['protocol']);
  let name = consumeKind('identifier').text;
  let result = new ProtocolDecl(origin, name);
  if (tryConsume([':']) != null) {
    while (test(['{']) == null) {
      result.addExtends(parseProtocolRef());
      if (tryConsume([',']) == null) {
        break;
      }
    }
  }
  consume(['{']);
  while (tryConsume(['}']) == null) {
    result.add(parseProtocolFuncDecl());
    consume([';']);
  }
  return result;
}

function parseField(): Field {
  let type = parseType();
  let name = consumeKind('identifier');
  consume([';']);
  return new Field(name, name.text, type);
}

function parseStructType(): StructType {
  let origin = consume(['struct']);
  let name: string = consumeKind('identifier').text;
  let typeParameters = parseTypeParameters();
  let result = new StructType(origin, name, typeParameters);
  consume(['{']);
  while (tryConsume(['}']) == null) {
    result.add(parseField());
  }
  return result;
}

function parseNativeFunc(): NativeFunc {
  let func1 = parseFuncDecl();
  consume([';']);
  return new NativeFunc(func1.origin!, func1.name, func1.returnType, func1.typeParameters as TypeVariable[], func1.parameters, func1.isCast, func1.shaderType);
}
function parseNative(): Node {
  let origin = consume(['native']);
  if (tryConsume(['typedef']) != null) {
    let name = consumeKind('identifier');
    let parameters = parseTypeParameters();
    consume([';']);
    return new NativeType(origin, name.text, parameters);
  }
  return parseNativeFunc();
}

function parseRestrictedFuncDef(): Func {
  consume(['restricted']);
  let result: Func;
  if (tryConsume(['native']) != null) {
    result = parseNativeFunc();
  } else {
    result = parseFuncDef();
  }
  result.isRestricted = true;
  return result;
}

function parseEnumMember(): EnumMember {
  let name = consumeKind('identifier');
  let value: null | Expression = null;
  if (tryConsume(['=']) != null) {
    value = parseConstexpr();
  }
  return new EnumMember(name, name.text, value);
}

function parseEnumType(): EnumType {
  consume(['enum']);
  let name = consumeKind('identifier');
  let baseType: HmType;
  if (tryConsume([':']) != null) {
    baseType = parseType();
  } else {
    baseType = new TypeRef(name, 'int', []);
  }
  consume(['{']);
  let result = new EnumType(name, name.text, baseType);
  while (test(['}']) == null) {
    result.add(parseEnumMember());
    if (tryConsume([',']) == null) {
      break;
    }
  }
  consume(['}']);
  return result;
}

class TernaryExpression extends Expression {
  constructor(origin: LexerToken, predicate: Node, left: Node, right: Node) {
    super(origin);
  }
}
class RValueFinder extends Base {
  _visit: (node: Node) => Node;

  constructor(visit: (node: Node) => Node) {
    super();
    this._visit = visit;
  }

  visitDotExpression(node: Node): void {
    (node as DotExpression).struct1!.visit(this);
  }

  visitIndexExpression(node: Node): void {
    let newNode = node as IndexExpression;
    newNode.array.visit(this);
    this._visit(newNode.index);
  }

  visitVariableRef(node: Node): void {}

  visitDereferenceExpression(node: Node): void {
    this._visit((node as DereferenceExpression).ptr);
  }

  visitIdentityExpression(node: Node): void {
    (node as IdentityExpression).target.visit(this);
  }

  visitMakeArrayRefExpression(node: Node): void {
    this._visit((node as MakeArrayRefExpression).lValue!);
  }

  mapReturn(): Map<string, (node: Base) => void | Base | string | null> {
    let methods = new Map<string, (node: Base) => void | Base | string | null>();
    methods.set('visitDotExpression', this.visitDotExpression as (node: Base) => void);
    methods.set('visitIndexExpression', this.visitIndexExpression as (node: Base) => void);
    methods.set('visitVariableRef', this.visitVariableRef as (node: Base) => void);
    methods.set('visitDereferenceExpression', this.visitDereferenceExpression as (node: Base) => void);
    methods.set('visitIdentityExpression', this.visitIdentityExpression as (node: Base) => void);
    methods.set('visitMakeArrayRefExpression', this.visitMakeArrayRefExpression as (node: Base) => void);
    return methods;
  }
}
function createNameResolver(program: Program): NameResolver {
  return new NameResolver(program.globalNameContext);
}

function resolveNamesInTypes(program: Program, nameResolver: NameResolver): void {
  for (let type of program.types.values()) {
    nameResolver.doStatement(type);
  }
}

function resolveNamesInProtocols(program: Program, nameResolver: NameResolver): void {
  for (let proto of program.protocols.values()) {
    nameResolver.doStatement(proto as ProtocolDecl);
  }
}

function resolveNamesInFunctions(program: Program, nameResolver: NameResolver): void {
  for (let funcs of program.functions.values()) {
    for (let func1 of funcs) {
      nameResolver.doStatement(func1 as FuncDef);
    }
  }
}

function resolveOverloadImpl(
  functions: Func[],
  typeArguments: Node[],
  argumentTypes: Node[],
  returnType: TypeRef | null,
  allowEntryPoint: boolean = false
): ResolveOverloadResult {
  if (functions.length === 0) {
    throw new Error('Null functions; that should have been caught by the caller.');
  }
  let failures: OverloadResolutionFailure[] = [];
  let successes: OverloadResolutionSuccess[] = [];
  for (let func1 of functions) {
    if (allowEntryPoint === false && func1.shaderType !== null) {
      failures.push(new OverloadResolutionFailure(func1, `Function is a ${func1.shaderType} shader, so it cannot be called from within an existing shader.`));
      continue;
    }
    let overload = inferTypesForCall(func1, typeArguments, argumentTypes, returnType);
    if (overload instanceof ResolveOverloadResult && overload.failure != null) {
      failures.push(overload.failure);
    } else {
      successes.push(overload as OverloadResolutionSuccess);
    }
  }

  if (successes.length === 0) {
    return new ResolveOverloadResult(null, failures, null, null, null, null);
  }

  let minimumConversionCost = successes.reduce((result, overload) => Math.min(result, overload.unificationContext.conversionCost), Number.POSITIVE_INFINITY);
  successes = successes.filter((overload) => overload.unificationContext.conversionCost === minimumConversionCost);

  // If any of the signatures are restricted then we consider those first. This is an escape mechanism for
  // built-in things.
  // FIXME: It should be an error to declare a function that is at least as specific as a restricted function.
  // https://bugs.webkit.org/show_bug.cgi?id=176580
  let hasRestricted = successes.reduce((result: boolean, overload: OverloadResolutionSuccess) => result || overload.func1!.isRestricted, false);
  if (hasRestricted) {
    successes = successes.filter((overload: OverloadResolutionSuccess) => overload.func1!.isRestricted);
  }

  // We are only interested in functions that are at least as specific as all of the others. This means
  // that they can be "turned around" and applied onto all of the other functions in the list.
  let prunedSuccesses: OverloadResolutionSuccess[] = [];
  for (let i = 0; i < successes.length; ++i) {
    let ok = true;
    let argumentFunc = successes[i].func1;
    for (let j = 0; j < successes.length; ++j) {
      if (i === j) {
        continue;
      }
      let parameterFunc: Func = successes[j].func1!;
      let overload = inferTypesForCall(
        parameterFunc,
        typeArguments.length > 0 ? argumentFunc!.typeParameters : [],
        argumentFunc!.parameterTypes,
        argumentFunc!.returnTypeForOverloadResolution as TypeRef | null
      );
      if ((overload as ResolveOverloadResult).func1 == null || (overload as OverloadResolutionSuccess).func1 == null) {
        ok = false;
        break;
      }
    }
    if (ok) {
      prunedSuccesses.push(successes[i]);
    }
  }

  if (prunedSuccesses.length === 1) {
    return new ResolveOverloadResult(
      null,
      null,
      prunedSuccesses[0],
      null,
      prunedSuccesses[0].func1,
      prunedSuccesses[0].unificationContext,
      prunedSuccesses[0].typeArguments
    );
  }
  let ambiguityList: OverloadResolutionSuccess[];
  let message: string;
  if (prunedSuccesses.length === 0) {
    ambiguityList = successes;
    message = 'Ambiguous overload - no function can be applied to all others';
  } else {
    ambiguityList = prunedSuccesses;
    message = 'Ambiguous overload - functions mutually applicable';
  }

  return new ResolveOverloadResult(
    null,
    ambiguityList.map((overload: OverloadResolutionSuccess): OverloadResolutionFailure => {
      return new OverloadResolutionFailure(overload.func1!, message);
    }),
    null,
    null,
    null,
    null
  );
}

class OverloadResolutionSuccess {
  func1: Func | null;
  unificationContext: UnificationContext;
  typeArguments: Node[];

  constructor(func1: Func | null, unificationContext: UnificationContext, typeArguments: Node[]) {
    this.func1 = func1;
    this.unificationContext = unificationContext;
    this.typeArguments = typeArguments;
  }
}

class ResolveOverloadResult {
  failure: OverloadResolutionFailure | null;
  failures: OverloadResolutionFailure[] | null;
  success: OverloadResolutionSuccess | null;
  successes: OverloadResolutionSuccess[] | null;
  func1: Func | null;
  unificationContext: UnificationContext | null;
  typeArguments: Node[] | null = null;

  constructor(
    failure: OverloadResolutionFailure | null,
    failures: OverloadResolutionFailure[] | null,
    success: OverloadResolutionSuccess | null,
    successes: OverloadResolutionSuccess[] | null,
    func1: Func | null,
    unificationContext: UnificationContext | null,
    typeArguments: Node[] | null = null
  ) {
    this.failure = failure;
    this.failures = failures;
    this.success = success;
    this.successes = successes;
    this.func1 = func1;
    this.unificationContext = unificationContext;
    this.typeArguments = typeArguments;
  }
}

function resolveProperties(program: Program): void {
  program.visit(new PropertyResolver());
}

function resolveTypeDefsInTypes(program: Program): void {
  let resolver = new TypeDefResolver();
  for (let type of program.types.values()) {
    type.visit(resolver);
  }
}

function resolveTypeDefsInProtocols(program: Program): void {
  let resolver = new TypeDefResolver();
  for (let proto of program.protocols.values()) {
    proto.visit(resolver);
  }
}

function resolveTypeDefsInFunctions(program: Program): void {
  let resolver = new TypeDefResolver();
  for (let funcs of program.functions.values()) {
    for (let func1 of funcs) {
      func1.visit(resolver);
    }
  }
}

class ReturnStyleType {
  definitelyReturns: string = '';
  definitelyDoesntReturn: string = '';
  hasntReturnedYet: string = '';
}

// NOTE: The next line is line 28, and we rely on this in Prepare.js.
let standardLibrary = `

// This is the WSL standard library. Implementations of all of these things are in
// Intrinsics.js.

// Need to bootstrap void first.
native typedef void;

native typedef uint8;
native typedef int32;
native typedef uint32;
native typedef bool;
typedef int = int32;
typedef uint = uint32;

native typedef float32;
native typedef float64;
typedef float = float32;
typedef double = float64;

native operator int32(uint32);
native operator int32(uint8);
native operator int32(float);
native operator int32(double);
native operator uint32(int32);
native operator uint32(uint8);
native operator uint32(float);
native operator uint32(double);
native operator uint8(int32);
native operator uint8(uint32);
native operator uint8(float);
native operator uint8(double);
native operator float(int32);
native operator float(uint32);
native operator float(uint8);
native operator float(double);
native operator double(float);
native operator double(int32);
native operator double(uint32);
native operator double(uint8);

native int operator+(int, int);
native uint operator+(uint, uint);
uint8 operator+(uint8 a, uint8 b) { return uint8(uint(a) + uint(b)); }
native float operator+(float, float);
native double operator+(double, double);
int operator++(int value) { return value + 1; }
uint operator++(uint value) { return value + 1; }
uint8 operator++(uint8 value) { return value + 1; }
native int operator-(int, int);
native uint operator-(uint, uint);
uint8 operator-(uint8 a, uint8 b) { return uint8(uint(a) - uint(b)); }
native float operator-(float, float);
native double operator-(double, double);
int operator--(int value) { return value - 1; }
uint operator--(uint value) { return value - 1; }
uint8 operator--(uint8 value) { return value - 1; }
native int operator*(int, int);
native uint operator*(uint, uint);
uint8 operator*(uint8 a, uint8 b) { return uint8(uint(a) * uint(b)); }
native float operator*(float, float);
native double operator*(double, double);
native int operator/(int, int);
native uint operator/(uint, uint);
uint8 operator/(uint8 a, uint8 b) { return uint8(uint(a) / uint(b)); }
native int operator&(int, int);
native int operator|(int, int);
native int operator^(int, int);
native int operator~(int);
native int operator<<(int, uint);
native int operator>>(int, uint);
native uint operator&(uint, uint);
native uint operator|(uint, uint);
native uint operator^(uint, uint);
native uint operator~(uint);
native uint operator<<(uint, uint);
native uint operator>>(uint, uint);
uint8 operator&(uint8 a, uint8 b) { return uint8(uint(a) & uint(b)); }
uint8 operator|(uint8 a, uint8 b) { return uint8(uint(a) | uint(b)); }
uint8 operator^(uint8 a, uint8 b) { return uint8(uint(a) ^ uint(b)); }
uint8 operator~(uint8 value) { return uint8(~uint(value)); }
uint8 operator<<(uint8 a, uint b) { return uint8(uint(a) << (b & 7)); }
uint8 operator>>(uint8 a, uint b) { return uint8(uint(a) >> (b & 7)); }
native float operator/(float, float);
native double operator/(double, double);
native bool operator==(int, int);
native bool operator==(uint, uint);
bool operator==(uint8 a, uint8 b) { return uint(a) == uint(b); }
native bool operator==(bool, bool);
native bool operator==(float, float);
native bool operator==(double, double);
native bool operator<(int, int);
native bool operator<(uint, uint);
bool operator<(uint8 a, uint8 b) { return uint(a) < uint(b); }
native bool operator<(float, float);
native bool operator<(double, double);
native bool operator<=(int, int);
native bool operator<=(uint, uint);
bool operator<=(uint8 a, uint8 b) { return uint(a) <= uint(b); }
native bool operator<=(float, float);
native bool operator<=(double, double);
native bool operator>(int, int);
native bool operator>(uint, uint);
bool operator>(uint8 a, uint8 b) { return uint(a) > uint(b); }
native bool operator>(float, float);
native bool operator>(double, double);
native bool operator>=(int, int);
native bool operator>=(uint, uint);
bool operator>=(uint8 a, uint8 b) { return uint(a) >= uint(b); }
native bool operator>=(float, float);
native bool operator>=(double, double);

bool operator&(bool a, bool b)
{
    if (a)
        return b;
    return false;
}

bool operator|(bool a, bool b)
{
    if (a)
        return true;
    if (b)
        return true;
    return false;
}

bool operator^(bool a, bool b)
{
    if (a)
        return !b;
    return b;
}

bool operator~(bool value)
{
    return !value;
}

protocol Addable {
    Addable operator+(Addable, Addable);
}

protocol Equatable {
    bool operator==(Equatable, Equatable);
}

restricted operator<T> T()
{
    T defaultValue;
    return defaultValue;
}

restricted operator<T> T(T x)
{
    return x;
}

operator<T:Equatable> bool(T x)
{
    return x != T();
}

struct vec2<T> {
    T x;
    T y;
}

typedef int2 = vec2<int>;
typedef uint2 = vec2<uint>;
typedef float2 = vec2<float>;
typedef double2 = vec2<double>;

operator<T> vec2<T>(T x, T y)
{
    vec2<T> result;
    result.x = x;
    result.y = y;
    return result;
}

bool operator==<T:Equatable>(vec2<T> a, vec2<T> b)
{
    return a.x == b.x && a.y == b.y;
}

thread T* operator&[]<T>(thread vec2<T>* foo, uint index)
{
    if (index == 0)
        return &foo->x;
    if (index == 1)
        return &foo->y;
    trap;
}

struct vec3<T> {
    T x;
    T y;
    T z;
}

typedef int3 = vec3<int>;
typedef uint3 = vec3<uint>;
typedef float3 = vec3<float>;
typedef double3 = vec3<double>;

operator<T> vec3<T>(T x, T y, T z)
{
    vec3<T> result;
    result.x = x;
    result.y = y;
    result.z = z;
    return result;
}

operator<T> vec3<T>(vec2<T> v2, T z)
{
    vec3<T> result;
    result.x = v2.x;
    result.y = v2.y;
    result.z = z;
    return result;
}

operator<T> vec3<T>(T x, vec2<T> v2)
{
    vec3<T> result;
    result.x = x;
    result.y = v2.x;
    result.z = v2.y;
    return result;
}

bool operator==<T:Equatable>(vec3<T> a, vec3<T> b)
{
    return a.x == b.x && a.y == b.y && a.z == b.z;
}

thread T* operator&[]<T>(thread vec3<T>* foo, uint index)
{
    if (index == 0)
        return &foo->x;
    if (index == 1)
        return &foo->y;
    if (index == 2)
        return &foo->z;
    trap;
}

struct vec4<T> {
    T x;
    T y;
    T z;
    T w;
}

typedef int4 = vec4<int>;
typedef uint4 = vec4<uint>;
typedef float4 = vec4<float>;
typedef double4 = vec4<double>;

operator<T> vec4<T>(T x, T y, T z, T w)
{
    vec4<T> result;
    result.x = x;
    result.y = y;
    result.z = z;
    result.w = w;
    return result;
}

operator<T> vec4<T>(vec2<T> v2, T z, T w)
{
    vec4<T> result;
    result.x = v2.x;
    result.y = v2.y;
    result.z = z;
    result.w = w;
    return result;
}

operator<T> vec4<T>(T x, vec2<T> v2, T w)
{
    vec4<T> result;
    result.x = x;
    result.y = v2.x;
    result.z = v2.y;
    result.w = w;
    return result;
}

operator<T> vec4<T>(T x, T y, vec2<T> v2)
{
    vec4<T> result;
    result.x = x;
    result.y = y;
    result.z = v2.x;
    result.w = v2.y;
    return result;
}

operator<T> vec4<T>(vec2<T> v2a, vec2<T> v2b)
{
    vec4<T> result;
    result.x = v2a.x;
    result.y = v2a.y;
    result.z = v2b.x;
    result.w = v2b.y;
    return result;
}

operator<T> vec4<T>(vec3<T> v3, T w)
{
    vec4<T> result;
    result.x = v3.x;
    result.y = v3.y;
    result.z = v3.z;
    result.w = w;
    return result;
}

operator<T> vec4<T>(T x, vec3<T> v3)
{
    vec4<T> result;
    result.x = x;
    result.y = v3.x;
    result.z = v3.y;
    result.w = v3.z;
    return result;
}

bool operator==<T:Equatable>(vec4<T> a, vec4<T> b)
{
    return a.x == b.x && a.y == b.y && a.z == b.z && a.w == b.w;
}

thread T* operator&[]<T>(thread vec4<T>* foo, uint index)
{
    if (index == 0)
        return &foo->x;
    if (index == 1)
        return &foo->y;
    if (index == 2)
        return &foo->z;
    if (index == 3)
        return &foo->w;
    trap;
}

native thread T* operator&[]<T>(thread T[], uint);
native threadgroup T* operator&[]<T>(threadgroup T[], uint);
native device T* operator&[]<T>(device T[], uint);
native constant T* operator&[]<T>(constant T[], uint);

native uint operator.length<T>(thread T[]);
native uint operator.length<T>(threadgroup T[]);
native uint operator.length<T>(device T[]);
native uint operator.length<T>(constant T[]);

uint operator.length<T, uint length>(T[length])
{
    return length;
}
`;

function intToString(x: number): string {
  switch (x) {
    case 0:
      return 'x';
    case 1:
      return 'y';
    case WSL_2:
      return 'z';
    case WSL_3:
      return 'w';
    default:
      throw new Error('Could not generate standard library.');
  }
}

// There are 481 swizzle operators. Let's not list them explicitly.
function generateSwizzle(maxDepth: number, maxItems: number, array: null | string[]): string {
  let newArray = array ?? [];
  if (newArray.length === maxDepth) {
    let result = `vec${newArray.length}<T> operator.${newArray.join('')}<T>(vec${maxItems}<T> v)
{
    vec${newArray.length}<T> result;
`;
    for (let i = 0; i < newArray.length; i++) {
      result += `    result.${intToString(i)} = v.${newArray[i]};`;
    }
    result += `    return result;
}`;
    return result;
  }
  let result = '';
  for (let i = 0; i < maxItems; i++) {
    newArray.push(intToString(i));
    result += generateSwizzle(maxDepth, maxItems, newArray);
    newArray.pop();
  }
  return result;
}

function forMaxDepth(): void {
  for (let maxDepth = WSL_2; maxDepth <= WSL_2; maxDepth++) {
    for (let maxItems = WSL_2; maxItems <= WSL_2; maxItems++) {
      standardLibrary += generateSwizzle(maxDepth, maxItems, null);
    }
  }
}

function synthesizeEnumFunctions(program: Program): void {
  for (let type of program.types.values()) {
    if (!(type instanceof EnumType)) {
      continue;
    }
    let nativeFunc: NativeFunc;
    let isCast = false;
    let shaderType: string | null = null;

    nativeFunc = new NativeFunc(
      type.origin!,
      'operator==',
      new TypeRef(type.origin!, 'bool', []),
      [],
      [
        new FuncParameter(type.origin!, null, new TypeRef(type.origin!, type.name, [])),
        new FuncParameter(type.origin!, null, new TypeRef(type.origin!, type.name, []))
      ],
      isCast,
      shaderType!
    );
    nativeFunc.implementation = (arr: EPtr[]): EPtr => EPtr.box((arr[0].loadValue() as number) === (arr[1].loadValue() as number));
    program.add(nativeFunc);

    nativeFunc = new NativeFunc(
      type.origin!,
      'operator.value',
      (type as EnumType).baseType.visit(new Rewriter()) as TypeRef,
      [],
      [new FuncParameter(type.origin!, null, new TypeRef(type.origin!, type.name, []))],
      isCast,
      shaderType
    );
    nativeFunc.implementation = (arr: EPtr[]): EPtr => arr[0];
    program.add(nativeFunc);

    nativeFunc = new NativeFunc(
      type.origin!,
      'operator cast',
      (type as EnumType).baseType.visit(new Rewriter()) as HmType,
      [],
      [new FuncParameter(type.origin!, null, new TypeRef(type.origin!, type.name, []))],
      isCast,
      shaderType
    );
    nativeFunc.implementation = (arr: EPtr[]): EPtr => arr[0];
    program.add(nativeFunc);

    nativeFunc = new NativeFunc(
      type.origin!,
      'operator cast',
      new TypeRef(type.origin!, type.name, []),
      [],
      [new FuncParameter(type.origin!, null, (type as EnumType).baseType.visit(new Rewriter()) as HmType)],
      isCast,
      shaderType
    );
    nativeFunc.implementation = (arr: EPtr[]): EPtr => arr[0];
    program.add(nativeFunc);
  }
}

function createTypeParameters(structType: StructType): TypeVariable[] {
  return structType.typeParameters.map((typeParameter: Node): TypeVariable => {
    return typeParameter.visit(new TypeParameterRewriter()) as TypeVariable;
  });
}

function createTypeArguments(typeParameters: TypeVariable[]): HmType[] {
  return typeParameters.map((typeParameter): HmType => typeParameter.visit(new AutoWrapper()) as HmType);
}

function setupImplementationData(
  structType: StructType,
  field: Field,
  nativeFunc: NativeFunc,
  implementation: (bases: EPtr[], offset: number, structSize: number, fieldSize: number) => EPtr
): void {
  nativeFunc.instantiateImplementation = (substitution: InstantiationSubstitution): ImplementationDataType => {
    let newType = structType.instantiate(
      nativeFunc.typeParameters.map((typeParameter: Node): HmType => {
        let substitute = substitution.map.get(typeParameter);
        if (substitute == null) {
          throw new Error(`Null substitute for type parameter ${typeParameter}`);
        }
        return substitute! as HmType;
      })
    );
    return new ImplementationDataType(newType, field.name);
  };

  nativeFunc.visitImplementationData = (implementationData, visitor): void => {
    // Visiting the type first ensures that the struct layout builder figures out the field's
    // offset.
    implementationData.hmType.visit(visitor);
  };
  nativeFunc.didLayoutStructsInImplementationData = (implementationData: ImplementationDataType): void => {
    let structSize: number | null = implementationData.hmType.size;
    if (structSize === -1) {
      throw new Error(`No struct size for ${nativeFunc}`);
    }
    let field = implementationData.hmType.fieldByName(implementationData.fieldName);
    if (field == null) {
      throw new Error('Could not find field');
    }
    let offset: number = field.offset;
    if (offset === -1) {
      throw new Error(`No offset for ${nativeFunc}`);
    }
    let fieldSize: number | null = field.hmType!.size;
    if (fieldSize === -1) {
      throw new Error(`No field size for ${nativeFunc}`);
    }
    implementationData.offset = offset;
    implementationData.structSize = structSize;
    implementationData.fieldSize = fieldSize;
  };

  nativeFunc.implementation = (argumentList, node): EPtr => {
    let nativeFuncInstance = node!.nativeFuncInstance as NativeFuncInstance;
    let implementationData = nativeFuncInstance.implementationData;
    return implementation(argumentList, implementationData.offset!, implementationData.structSize!, implementationData.fieldSize!);
  };
}

function createFieldType(structType: StructType, field: Field, typeParameters: TypeVariable[]): HmType {
  return field.hmType!.visit(new Substitution(structType.typeParameters, typeParameters)) as HmType;
}

function createTypeRef(structType: StructType, typeParameters: TypeVariable[]): TypeRef {
  return TypeRef.instantiate(structType, createTypeArguments(typeParameters));
}
function setupAnder(
  addressSpace: string,
  isCast: boolean,
  shaderType: string | null,
  program: Program,
  structType: StructType,
  field: Field,
  typeParameters: TypeVariable[]
): void {
  typeParameters = createTypeParameters(structType);
  let nativeFunc = new NativeFunc(
    field.origin!,
    `operator&.${field.name}`,
    new PtrType(field.origin!, addressSpace, createFieldType(structType, field, typeParameters)),
    typeParameters,
    [new FuncParameter(field.origin!, null, new PtrType(field.origin, addressSpace, createTypeRef(structType, typeParameters)))],
    isCast,
    shaderType!
  );
  setupImplementationData(structType, field, nativeFunc, (bases, offset, structSize, fieldSize) => {
    let base = bases[0].loadValue() as EPtr;
    return EPtr.box(base.plus(offset));
  });
  program.add(nativeFunc);
}

function synthesizeStructAccessors(program: Program): void {
  for (let type of program.types.values()) {
    if (!(type instanceof StructType)) {
      continue;
    }
    let structType = type as StructType;
    for (let field of structType.fields) {
      let isCast = false;
      let shaderType: string | null = null;
      let typeParameters: TypeVariable[];
      let nativeFunc: NativeFunc;

      // The getter: operator.field
      typeParameters = createTypeParameters(structType);
      nativeFunc = new NativeFunc(
        field.origin!,
        `operator.${field.name}`,
        createFieldType(structType, field, typeParameters),
        typeParameters,
        [new FuncParameter(field.origin!, null, createTypeRef(structType, typeParameters))],
        isCast,
        shaderType!
      );
      setupImplementationData(structType, field, nativeFunc, (bases, offset, structSize, fieldSize): EPtr => {
        let result = new EPtr(new EBuffer(fieldSize), 0);
        result.copyFrom(bases[0].plus(offset), fieldSize);
        return result;
      });
      program.add(nativeFunc);

      // The setter: operator.field=
      typeParameters = createTypeParameters(structType);
      nativeFunc = new NativeFunc(
        field.origin!,
        `operator.${field.name}=`,
        createTypeRef(structType, typeParameters),
        typeParameters,
        [
          new FuncParameter(field.origin!, null, createTypeRef(structType, typeParameters)),
		  new FuncParameter(field.origin!, null, createFieldType(structType, field, typeParameters))
        ],
        isCast,
        shaderType!
      );
      setupImplementationData(structType, field, nativeFunc, (bases, offset, structSize, fieldSize): EPtr => {
        const base = bases[0];
        const value = bases[1];
        let result = new EPtr(new EBuffer(structSize), 0);
        result.copyFrom(base, structSize);
        result.plus(offset).copyFrom(value, fieldSize);
        return result;
      });
      program.add(nativeFunc);

      setupAnder('thread', isCast, shaderType!, program, structType, field, typeParameters);
      setupAnder('threadgroup', isCast, shaderType!, program, structType, field, typeParameters);
      setupAnder('device', isCast, shaderType!, program, structType, field, typeParameters);
      setupAnder('constant', isCast, shaderType!, program, structType, field, typeParameters);
    }
  }
}

let uintConfig = new Config(
  'UintLiteral',
  'uint',
  (origin: LexerToken, value: number): UintLiteral => {
    return new UintLiteral(origin, (-1 * value) >>> 0);
  },
  (origin: LexerToken, value: number): UintLiteralType => {
    return new UintLiteralType(origin, value);
  }
);

let uintConfigType = new ConfigType('uint', (unificationContext: UnificationContext, type: GenericLiteralType): VerifyResult => {
  let realThis = unificationContext.find(type) as NativeType;
  if (!realThis.isInt) {
    return new VerifyResult(false, `Cannot use uint literal with non-integer type ${realThis}`);
  }
  if (realThis.isSigned) {
    return new VerifyResult(false, `Cannot use uint literal with signed type ${realThis}`);
  }
  if (!realThis.canRepresent(type.value)) {
    return new VerifyResult(false, `Uint literal ${type.value} too large to be represented by type ${realThis}`);
  }
  return new VerifyResult(true);
});

class UintLiteralType extends GenericLiteralType {
  constructor(origin: LexerToken, value: number) {
    super(origin, value, uintConfigType);
  }
}

function doPrep(code: string): Program {
  return prepare('/internal/test', 0, code);
}

function doLex(code: string): LexerToken[] {
  let lexer = new Lexer('/internal/test', 'native', 0, code);
  let result: LexerToken[] = [];
  while (true) {
    let next = lexer.next();
    if (next == null) {
      return result;
    }
    result.push(next);
  }
  return result;
}

function makeInt(program: Program, value: number): TypedValue {
  return TypedValue.box(program.intrinsics.int32!, value);
}

function makeUint(program: Program, value: number): TypedValue {
  return TypedValue.box(program.intrinsics.uint32!, value);
}

function makeUint8(program: Program, value: number): TypedValue {
  return TypedValue.box(program.intrinsics.uint8!, value);
}

function makeBool(program: Program, value: boolean): TypedValue {
  return TypedValue.box(program.intrinsics.bool!, value);
}

function makeFloat(program: Program, value: number): TypedValue {
  return TypedValue.box(program.intrinsics.float!, value);
}

function makeDouble(program: Program, value: number): TypedValue {
  return TypedValue.box(program.intrinsics.double!, value);
}

function makeEnum(program: Program, enumName: string, value: string): TypedValue {
  let enumType: EnumType | null = program.types.get(enumName) as EnumType;
  if (enumType == null) {
    throw new Error(`No type named ${enumName}`);
  }
  let enumMember: EnumMember | null = enumType.memberByName(value);
  if (enumMember == null) {
    throw new Error(`No member named ${enumMember} in ${enumType}`);
  }
  return TypedValue.box(enumType, ((enumMember.value as GenericLiteral).unifyNode as GenericLiteral).valueForSelectedType);
}

function checkNumber(program: Program, result: TypedValue, expected: number): void {
  if (!(result.hmType.unifyNode as TypeRef).isNumber) {
    throw new Error(`Wrong result type; result: ${result}`);
  }
  if (result.value !== expected) {
    throw new Error(`Wrong result: ${result.value} (expected ${expected})`);
  }
}

function checkInt(program: Program, result: TypedValue, expected: number): void {
  if (!result.hmType.equals(program.intrinsics.int32!)) {
    throw new Error(`Wrong result type; result: ${result}`);
  }
  checkNumber(program, result, expected);
}

function checkEnum(program: Program, result: TypedValue, expected: number): void {
  if (!(result.hmType.unifyNode instanceof EnumType)) {
    throw new Error(`Wrong result type; result: ${result}`);
  }
  if (result.value !== expected) {
    throw new Error(`Wrong result: ${result.value} (expected ${expected})`);
  }
}

function checkUint(program: Program, result: TypedValue, expected: number): void {
  if (!result.hmType.equals(program.intrinsics.uint32!)) {
    throw new Error(`Wrong result type: ${result.hmType}`);
  }
  if (result.value !== expected) {
    throw new Error(`Wrong result: ${result.value} (expected ${expected})`);
  }
}

function checkUint8(program: Program, result: TypedValue, expected: number): void {
  if (!result.hmType.equals(program.intrinsics.uint8!)) {
    throw new Error(`Wrong result type: ${result.hmType}`);
  }
  if (result.value !== expected) {
    throw new Error(`Wrong result: ${result.value} (expected ${expected})`);
  }
}

function checkBool(program: Program, result: TypedValue, expected: boolean): void {
  if (!result.hmType.equals(program.intrinsics.bool!)) {
    throw new Error(`Wrong result type: ${result.hmType}`);
  }
  if (result.value !== expected) {
    throw new Error(`Wrong result: ${result.value} (expected ${expected})`);
  }
}

function checkFloat(program: Program, result: TypedValue, expected: number): void {
  if (!result.hmType.equals(program.intrinsics.float!)) {
    throw new Error(`Wrong result type: ${result.hmType}`);
  }
  if (result.value !== expected) {
    throw new Error(`Wrong result: ${result.value} (expected ${expected})`);
  }
}

function checkDouble(program: Program, result: TypedValue, expected: number): void {
  if (!result.hmType.equals(program.intrinsics.double!)) {
    throw new Error(`Wrong result type: ${result.hmType}`);
  }
  if (result.value !== expected) {
    throw new Error(`Wrong result: ${result.value} (expected ${expected})`);
  }
}

function checkLexerToken(result: LexerToken, expectedIndex: number, expectedKind: string, expectedText: string): void {
  if (result.index !== expectedIndex) {
    throw new Error(`Wrong lexer index; result: ${result.index} (expected ${expectedIndex})`);
  }
  if (result.kind !== expectedKind) {
    throw new Error(`Wrong lexer kind; result: ${result.kind} (expected ${expectedKind})`);
  }
  if (result.text !== expectedText) {
    throw new Error(`Wrong lexer text; result: ${result.text} (expected ${expectedText})`);
  }
}

function checkFail(callback: () => Program | TypedValue | void, predicate: (e: Error) => boolean): void {
  try {
    callback();
    throw new Error('Did not throw exception');
  } catch (e) {
    if (predicate(e as Error)) {
      return;
    }
    throw e as Error;
  }
}

let okToTest = false;

function getTests(): Map<string, () => void> {
  let tests: Map<string, () => void> = new Map();

  tests.set('literalBool', () => {
    let program = doPrep('bool foo() { return true; }');
    checkBool(program, callFunction(program, 'foo', [], []), true);
  });

  tests.set('identityBool', () => {
    let program = doPrep('bool foo(bool x) { return x; }');
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false)]), false);
  });

  tests.set('intSimpleMath', () => {
    let program = doPrep('int foo(int x, int y) { return x + y; }');
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7), makeInt(program, WSL_5)]), WSL_12);
    program = doPrep('int foo(int x, int y) { return x - y; }');
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7), makeInt(program, WSL_5)]), WSL_2);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5), makeInt(program, WSL_7)]), WSLNEGATIVE_2);
    program = doPrep('int foo(int x, int y) { return x * y; }');
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7), makeInt(program, WSL_5)]), WSL_35);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7), makeInt(program, WSLNEGATIVE_5)]), WSLNEGATIVE_35);
    program = doPrep('int foo(int x, int y) { return x / y; }');
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7), makeInt(program, WSL_2)]), WSL_3);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7), makeInt(program, WSLNEGATIVE_2)]), WSLNEGATIVE_3);
  });

  tests.set('uintSimpleMath', () => {
    let program = doPrep('uint foo(uint x, uint y) { return x + y; }');
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, WSL_7), makeUint(program, WSL_5)]), WSL_12);
    program = doPrep('uint foo(uint x, uint y) { return x - y; }');
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, WSL_7), makeUint(program, WSL_5)]), WSL_2);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, WSL_5), makeUint(program, WSL_7)]), WSL_4294967294);
    program = doPrep('uint foo(uint x, uint y) { return x * y; }');
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, WSL_7), makeUint(program, WSL_5)]), WSL_35);
    program = doPrep('uint foo(uint x, uint y) { return x / y; }');
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, WSL_7), makeUint(program, WSL_2)]), WSL_3);
  });

  tests.set('uint8SimpleMath', () => {
    let program = doPrep('uint8 foo(uint8 x, uint8 y) { return x + y; }');
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_7), makeUint8(program, WSL_5)]), WSL_12);
    program = doPrep('uint8 foo(uint8 x, uint8 y) { return x - y; }');
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_7), makeUint8(program, WSL_5)]), WSL_2);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_5), makeUint8(program, WSL_7)]), WSL_254);
    program = doPrep('uint8 foo(uint8 x, uint8 y) { return x * y; }');
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_7), makeUint8(program, WSL_5)]), WSL_35);
    program = doPrep('uint8 foo(uint8 x, uint8 y) { return x / y; }');
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_7), makeUint8(program, WSL_2)]), WSL_3);
  });

  tests.set('equality', () => {
    let program = doPrep('bool foo(uint x, uint y) { return x == y; }');
    checkBool(program, callFunction(program, 'foo', [], [makeUint(program, WSL_7), makeUint(program, WSL_5)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeUint(program, WSL_7), makeUint(program, WSL_7)]), true);
    program = doPrep('bool foo(uint8 x, uint8 y) { return x == y; }');
    checkBool(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_7), makeUint8(program, WSL_5)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_7), makeUint8(program, WSL_7)]), true);
    program = doPrep('bool foo(int x, int y) { return x == y; }');
    checkBool(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7), makeInt(program, WSL_5)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7), makeInt(program, WSL_7)]), true);
    program = doPrep('bool foo(bool x, bool y) { return x == y; }');
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false), makeBool(program, true)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true), makeBool(program, false)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false), makeBool(program, false)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true), makeBool(program, true)]), true);
  });

  tests.set('logicalNegation', () => {
    let program = doPrep('bool foo(bool x) { return !x; }');
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false)]), true);
  });

  tests.set('notEquality', () => {
    let program = doPrep('bool foo(uint x, uint y) { return x != y; }');
    checkBool(program, callFunction(program, 'foo', [], [makeUint(program, WSL_7), makeUint(program, WSL_5)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeUint(program, WSL_7), makeUint(program, WSL_7)]), false);
    program = doPrep('bool foo(uint8 x, uint8 y) { return x != y; }');
    checkBool(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_7), makeUint8(program, WSL_5)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_7), makeUint8(program, WSL_7)]), false);
    program = doPrep('bool foo(int x, int y) { return x != y; }');
    checkBool(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7), makeInt(program, WSL_5)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7), makeInt(program, WSL_7)]), false);
    program = doPrep('bool foo(bool x, bool y) { return x != y; }');
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false), makeBool(program, true)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true), makeBool(program, false)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false), makeBool(program, false)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true), makeBool(program, true)]), false);
  });

  tests.set('equalityTypeFailure', () => {
    checkFail(
      () => doPrep('bool foo(int x, uint y) { return x == y; }'),
      (e) => e instanceof WTypeError && e.message.indexOf('/internal/test:1') !== -1
    );
  });

  tests.set('generalNegation', () => {
    let program = doPrep('bool foo(int x) { return !x; }');
    checkBool(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeInt(program, 0)]), true);
  });

  tests.set('add1', () => {
    let program = doPrep('int foo(int x) { return x + 1; }');
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_42)]), WSL_43);
  });

  tests.set('simpleGeneric', () => {
    let program = doPrep(`
          T id<T>(T x) { return x; }
          int foo(int x) { return id(x) + 1; }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_42)]), WSL_43);
  });

  tests.set('nameResolutionFailure', () => {
    checkFail(
      () => doPrep('int foo(int x) { return x + y; }'),
      (e) => e instanceof WTypeError && e.message.indexOf('/internal/test:1') !== -1
    );
  });

  tests.set('simpleVariable', () => {
    let program = doPrep(`
          int foo(int p)
          {
              int result = p;
              return result;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_42)]), WSL_42);
  });

  tests.set('simpleAssignment', () => {
    let program = doPrep(`
          int foo(int p)
          {
              int result;
              result = p;
              return result;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_42)]), WSL_42);
  });

  tests.set('simpleDefault', () => {
    let program = doPrep(`
          int foo()
          {
              int result;
              return result;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), 0);
  });

  tests.set('simpleDereference', () => {
    let program = doPrep(`
          int foo(device int* p)
          {
              return *p;
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_13);
    checkInt(
      program,
      callFunction(program, 'foo', [], [TypedValue.box(new PtrType(externalOrigin, 'device', program.intrinsics.int32!), new EPtr(buffer, 0))]),
      WSL_13
    );
  });

  tests.set('dereferenceStore', () => {
    let program = doPrep(`
          void foo(device int* p)
          {
              *p = 52;
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_13);
    callFunction(program, 'foo', [], [TypedValue.box(new PtrType(externalOrigin, 'device', program.intrinsics.int32!), new EPtr(buffer, 0))]);
    let result = buffer.eBufferGet(0);

    if (result !== WSL_52) {
      throw new Error(`Expected buffer to contain 52 but it contains: ${result}`);
    }
  });

  tests.set('simpleMakePtr', () => {
    let program = doPrep(`
          thread int* foo()
          {
              int x = 42;
              return &x;
          }
      `);
    let result = callFunction(program, 'foo', [], []);
    if (!result.hmType.isPtr) {
      throw new Error(`Return type is not a pointer: ${result.hmType}`);
    }
    if (!(result.hmType as ArrayRefType | ReferenceType).elementType?.equals(program.intrinsics.int32!)) {
      throw new Error(`Return type is not a pointer to an int: ${result.hmType}`);
    }
    if (!(result.value instanceof EPtr)) {
      throw new Error(`Return value is not an EPtr: ${result.value}`);
    }
    let value = result.value.loadValue();
    if (value !== WSL_42) {
      throw new Error(`Expected 42 but got: ${value}`);
    }
  });

  tests.set('threadArrayLoad', () => {
    let program = doPrep(`
          int foo(thread int[] array)
          {
              return array[0u];
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_89);
    let result = callFunction(
      program,
      'foo',
      [],
      [TypedValue.box(new ArrayRefType(externalOrigin, 'thread', program.intrinsics.int32!), new EArrayRef(new EPtr(buffer, 0), 1))]
    );
    checkInt(program, result, WSL_89);
  });

  tests.set('threadArrayLoadIntLiteral', () => {
    let program = doPrep(`
          int foo(thread int[] array)
          {
              return array[0];
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_89);
    let result = callFunction(
      program,
      'foo',
      [],
      [TypedValue.box(new ArrayRefType(externalOrigin, 'thread', program.intrinsics.int32!), new EArrayRef(new EPtr(buffer, 0), 1))]
    );
    checkInt(program, result, WSL_89);
  });

  tests.set('deviceArrayLoad', () => {
    let program = doPrep(`
          int foo(device int[] array)
          {
              return array[0u];
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_89);
    let result = callFunction(
      program,
      'foo',
      [],
      [TypedValue.box(new ArrayRefType(externalOrigin, 'device', program.intrinsics.int32!), new EArrayRef(new EPtr(buffer, 0), 1))]
    );
    checkInt(program, result, WSL_89);
  });

  tests.set('threadArrayStore', () => {
    let program = doPrep(`
          void foo(thread int[] array, int value)
          {
              array[0u] = value;
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_15);
    let arrayRef = TypedValue.box(new ArrayRefType(externalOrigin, 'thread', program.intrinsics.int32!), new EArrayRef(new EPtr(buffer, 0), 1));
    callFunction(program, 'foo', [], [arrayRef, makeInt(program, WSL_65)]);
    if (buffer.eBufferGet(0) !== WSL_65) {
      throw new Error(`Bad value stored into buffer (expected 65): ${buffer.eBufferGet(0)}`);
    }
    callFunction(program, 'foo', [], [arrayRef, makeInt(program, WSLNEGATIVE_111)]);
    if (buffer.eBufferGet(0) !== WSLNEGATIVE_111) {
      throw new Error(`Bad value stored into buffer (expected -111): ${buffer.eBufferGet(0)}`);
    }
  });

  tests.set('deviceArrayStore', () => {
    let program = doPrep(`
          void foo(device int[] array, int value)
          {
              array[0u] = value;
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_15);
    let arrayRef = TypedValue.box(new ArrayRefType(externalOrigin, 'device', program.intrinsics.int32!), new EArrayRef(new EPtr(buffer, 0), 1));
    callFunction(program, 'foo', [], [arrayRef, makeInt(program, WSL_65)]);
    if (buffer.eBufferGet(0) !== WSL_65) {
      throw new Error(`Bad value stored into buffer (expected 65): ${buffer.eBufferGet(0)}`);
    }
    callFunction(program, 'foo', [], [arrayRef, makeInt(program, WSLNEGATIVE_111)]);
    if (buffer.eBufferGet(0) !== WSLNEGATIVE_111) {
      throw new Error(`Bad value stored into buffer (expected -111): ${buffer.eBufferGet(0)}`);
    }
  });

  tests.set('deviceArrayStoreIntLiteral', () => {
    let program = doPrep(`
          void foo(device int[] array, int value)
          {
              array[0] = value;
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_15);
    let arrayRef = TypedValue.box(new ArrayRefType(externalOrigin, 'device', program.intrinsics.int32!), new EArrayRef(new EPtr(buffer, 0), 1));
    callFunction(program, 'foo', [], [arrayRef, makeInt(program, WSL_65)]);
    if (buffer.eBufferGet(0) !== WSL_65) {
      throw new Error(`Bad value stored into buffer (expected 65): ${buffer.eBufferGet(0)}`);
    }
    callFunction(program, 'foo', [], [arrayRef, makeInt(program, WSLNEGATIVE_111)]);
    if (buffer.eBufferGet(0) !== WSLNEGATIVE_111) {
      throw new Error(`Bad value stored into buffer (expected -111): ${buffer.eBufferGet(0)}`);
    }
  });

  tests.set('simpleProtocol', () => {
    let program = doPrep(`
          protocol MyAddable {
              MyAddable operator+(MyAddable, MyAddable);
          }
          T add<T:MyAddable>(T a, T b)
          {
              return a + b;
          }
          int foo(int x)
          {
              return add(x, 73);
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_45)]), WSL_45 + WSL_73);
  });

  tests.set('typeMismatchReturn', () => {
    checkFail(
      () =>
        doPrep(`
              int foo()
              {
                  return 0u;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('typeMismatchVariableDecl', () => {
    checkFail(
      () =>
        doPrep(`
              void foo(uint x)
              {
                  int y = x;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('typeMismatchAssignment', () => {
    checkFail(
      () =>
        doPrep(`
              void foo(uint x)
              {
                  int y;
                  y = x;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('typeMismatchReturnParam', () => {
    checkFail(
      () =>
        doPrep(`
              int foo(uint x)
              {
                  return x;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('badAdd', () => {
    checkFail(
      () =>
        doPrep(`
              void bar<T>(T) { }
              void foo(int x, uint y)
              {
                  bar(x + y);
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('native int32 operator+<>(int32,int32)') !== -1
    );
  });

  tests.set('lexerKeyword', () => {
    let result = doLex('ident for while 123 123u { } {asd asd{ 1a3 1.2 + 3.4 + 1. + .2 1.2d 0.d .3d && ||');
    if (result.length !== WSL_25) {
      throw new Error(`Lexer emitted an incorrect number of tokens (expected 23): ${result.length}`);
    }
    checkLexerToken(result[0], 0, 'identifier', 'ident');
    checkLexerToken(result[1], WSL_6, 'keyword', 'for');
    checkLexerToken(result[WSL_2], WSL_10, 'keyword', 'while');
    checkLexerToken(result[WSL_3], WSL_16, 'intLiteral', '123');
    checkLexerToken(result[WSL_4], WSL_20, 'uintLiteral', '123u');
    checkLexerToken(result[WSL_5], WSL_25, 'punctuation', '{');
    checkLexerToken(result[WSL_6], WSL_27, 'punctuation', '}');
    checkLexerToken(result[WSL_7], WSL_29, 'punctuation', '{');
    checkLexerToken(result[WSL_8], WSL_30, 'identifier', 'asd');
    checkLexerToken(result[WSL_9], WSL_34, 'identifier', 'asd');
    checkLexerToken(result[WSL_10], WSL_37, 'punctuation', '{');
    checkLexerToken(result[WSL_11], WSL_39, 'intLiteral', '1');
    checkLexerToken(result[WSL_12], WSL_40, 'identifier', 'a3');
    checkLexerToken(result[WSL_13], WSL_43, 'floatLiteral', '1.2');
    checkLexerToken(result[WSL_14], WSL_47, 'punctuation', '+');
    checkLexerToken(result[WSL_15], WSL_49, 'floatLiteral', '3.4');
    checkLexerToken(result[WSL_16], WSL_53, 'punctuation', '+');
    checkLexerToken(result[WSL_17], WSL_55, 'floatLiteral', '1.');
    checkLexerToken(result[WSL_18], WSL_58, 'punctuation', '+');
    checkLexerToken(result[WSL_19], WSL_60, 'floatLiteral', '.2');
    checkLexerToken(result[WSL_20], WSL_63, 'floatLiteral', '1.2d');
    checkLexerToken(result[WSL_21], WSL_68, 'floatLiteral', '0.d');
    checkLexerToken(result[WSL_22], WSL_72, 'floatLiteral', '.3d');
    checkLexerToken(result[WSL_23], WSL_76, 'punctuation', '&&');
    checkLexerToken(result[WSL_24], WSL_79, 'punctuation', '||');
  });

  tests.set('simpleNoReturn', () => {
    checkFail(
      () => doPrep('int foo() { }'),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('simpleUnreachableCode', () => {
    checkFail(
      () =>
        doPrep(`
              void foo()
              {
                  return;
                  int x;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('simpleStruct', () => {
    let program = doPrep(`
          struct Foo {
              int x;
              int y;
          }
          Foo foo(Foo foo)
          {
              Foo result;
              result.x = foo.y;
              result.y = foo.x;
              return result;
          }
      `);
    let structType: HmType = program.types.get('Foo')!;
    if (!structType) {
      throw new Error('Did not find Foo type');
    }
    let buffer = new EBuffer(WSL_2);
    buffer.eBufferSet(0, WSL_62);
    buffer.eBufferSet(1, WSL_24);
    let result = callFunction(program, 'foo', [], [new TypedValue(structType, new EPtr(buffer, 0))]);
    if (!result.hmType.equals(structType)) {
      throw new Error(`Wrong result type: ${result.hmType}`);
    }
    let x = result.ePtr!.ePtrGet(0);
    let y = result.ePtr!.ePtrGet(1);
    if (x !== WSL_24) {
      throw new Error(`Wrong result for x: ${x} (y = ${y})`);
    }
    if (y !== WSL_62) {
      throw new Error(`Wrong result for y: ${y} (x + ${x})`);
    }
  });

  tests.set('genericStructInstance', () => {
    let program = doPrep(`
          struct Foo<T> {
              T x;
              T y;
          }
          Foo<int> foo(Foo<int> foo)
          {
              Foo<int> result;
              result.x = foo.y;
              result.y = foo.x;
              return result;
          }
      `);
    let structType = TypeRef.instantiate(program.types.get('Foo') as StructType, [program.intrinsics.int32!]);
    let buffer = new EBuffer(WSL_2);
    buffer.eBufferSet(0, WSL_62);
    buffer.eBufferSet(1, WSL_24);
    let result = callFunction(program, 'foo', [], [new TypedValue(structType, new EPtr(buffer, 0))]);
    let x = result.ePtr!.ePtrGet(0);
    let y = result.ePtr!.ePtrGet(1);
    if (x !== WSL_24) {
      throw new Error(`Wrong result for x: ${x} (y = ${y})`);
    }
    if (y !== WSL_62) {
      throw new Error(`Wrong result for y: ${y} (x + ${x})`);
    }
  });

  tests.set('doubleGenericCallsDoubleGeneric', () => {
    doPrep(`
          void foo<T, U>(T, U) { }
          void bar<V, W>(V x, W y) { foo(x, y); }
      `);
  });

  tests.set('doubleGenericCallsSingleGeneric', () => {
    checkFail(
      () =>
        doPrep(`
              void foo<T>(T, T) { }
              void bar<V, W>(V x, W y) { foo(x, y); }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('loadNull', () => {
    checkFail(
      () =>
        doPrep(`
              void sink<T>(T) { }
              void foo() { sink(*null); }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Type passed to dereference is not a pointer: null') !== -1
    );
  });

  tests.set('storeNull', () => {
    checkFail(
      () =>
        doPrep(`
              void foo() { *null = 42; }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Type passed to dereference is not a pointer: null') !== -1
    );
  });

  tests.set('returnNull', () => {
    let program = doPrep(`
          thread int* foo() { return null; }
      `);
    let result = callFunction(program, 'foo', [], []);
    if (!result.hmType.isPtr) {
      throw new Error(`Return type is not a pointer: ${result.hmType}`);
    }
    if (!(result.hmType as ArrayRefType | ReferenceType).elementType?.equals(program.intrinsics.int32!)) {
      throw new Error(`Return type is not a pointer to an int: ${result.hmType}`);
    }
    if (result.value != null) {
      throw new Error(`Return value is not null: ${result.value}`);
    }
  });

  tests.set('dereferenceDefaultNull', () => {
    let program = doPrep(`
          int foo()
          {
              thread int* p;
              return *p;
          }
      `);
    checkFail(
      () => callFunction(program, 'foo', [], []),
      (e) => e instanceof WTrapError
    );
  });

  tests.set('defaultInitializedNull', () => {
    let program = doPrep(`
          int foo()
          {
              thread int* p = null;;
              return *p;
          }
      `);
    checkFail(
      () => callFunction(program, 'foo', [], []),
      (e) => e instanceof WTrapError
    );
  });

  tests.set('passNullToPtrMonomorphic', () => {
    let program = doPrep(`
          int foo(thread int* ptr)
          {
              return *ptr;
          }
          int bar()
          {
              return foo(null);
          }
      `);
    checkFail(
      () => callFunction(program, 'bar', [], []),
      (e) => e instanceof WTrapError
    );
  });

  tests.set('passNullToPtrPolymorphic', () => {
    checkFail(
      () =>
        doPrep(`
              T foo<T>(thread T* ptr)
              {
                  return *ptr;
              }
              int bar()
              {
                  return foo(null);
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('passNullToPolymorphic', () => {
    checkFail(
      () =>
        doPrep(`
              T foo<T>(T ptr)
              {
                  return ptr;
              }
              int bar()
              {
                  return foo(null);
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('loadNullArrayRef', () => {
    checkFail(
      () =>
        doPrep(`
              void sink<T>(T) { }
              void foo() { sink(null[0u]); }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Cannot resolve access') !== -1
    );
  });

  tests.set('storeNullArrayRef', () => {
    checkFail(
      () =>
        doPrep(`
              void foo() { null[0u] = 42; }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Cannot resolve access') !== -1
    );
  });

  tests.set('returnNullArrayRef', () => {
    let program = doPrep(`
          thread int[] foo() { return null; }
      `);
    let result = callFunction(program, 'foo', [], []);
    if (!result.hmType.isArrayRef) {
      throw new Error(`Return type is not an array reference: ${result.hmType}`);
    }
    if (!(result.hmType as ArrayRefType | ReferenceType).elementType?.equals(program.intrinsics.int32!)) {
      throw new Error(`Return type is not an int array reference: ${result.hmType}`);
    }
    if (result.value != null) {
      throw new Error(`Return value is not null: ${result.value}`);
    }
  });

  tests.set('dereferenceDefaultNullArrayRef', () => {
    let program = doPrep(`
          int foo()
          {
              thread int[] p;
              return p[0u];
          }
      `);
    checkFail(
      () => callFunction(program, 'foo', [], []),
      (e) => e instanceof WTrapError
    );
  });

  tests.set('defaultInitializedNullArrayRef', () => {
    let program = doPrep(`
          int foo()
          {
              thread int[] p = null;
              return p[0u];
          }
      `);
    checkFail(
      () => callFunction(program, 'foo', [], []),
      (e) => e instanceof WTrapError
    );
  });

  tests.set('defaultInitializedNullArrayRefIntLiteral', () => {
    let program = doPrep(`
          int foo()
          {
              thread int[] p = null;
              return p[0];
          }
      `);
    checkFail(
      () => callFunction(program, 'foo', [], []),
      (e) => e instanceof WTrapError
    );
  });

  tests.set('passNullToPtrMonomorphicArrayRef', () => {
    let program = doPrep(`
          int foo(thread int[] ptr)
          {
              return ptr[0u];
          }
          int bar()
          {
              return foo(null);
          }
      `);
    checkFail(
      () => callFunction(program, 'bar', [], []),
      (e) => e instanceof WTrapError
    );
  });

  tests.set('passNullToPtrPolymorphicArrayRef', () => {
    checkFail(
      () =>
        doPrep(`
              T foo<T>(thread T[] ptr)
              {
                  return ptr[0u];
              }
              int bar()
              {
                  return foo(null);
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('returnIntLiteralUint', () => {
    let program = doPrep('uint foo() { return 42; }');
    checkNumber(program, callFunction(program, 'foo', [], []), WSL_42);
  });

  tests.set('returnIntLiteralDouble', () => {
    let program = doPrep('double foo() { return 42; }');
    checkNumber(program, callFunction(program, 'foo', [], []), WSL_42);
  });

  tests.set('badIntLiteralForInt', () => {
    checkFail(
      () => doPrep('void foo() { int x = 3000000000; }'),
      (e) => e instanceof WSyntaxError
    );
  });

  tests.set('badIntLiteralForUint', () => {
    checkFail(
      () => doPrep('void foo() { uint x = 5000000000; }'),
      (e) => e instanceof WSyntaxError
    );
  });

  tests.set('badIntLiteralForDouble', () => {
    checkFail(
      () => doPrep('void foo() { double x = 5000000000000000000000000000000000000; }'),
      (e) => e instanceof WSyntaxError
    );
  });

  tests.set('passNullAndNotNull', () => {
    let program = doPrep(`
          T bar<T>(device T* p, device T*)
          {
              return *p;
          }
          int foo(device int* p)
          {
              return bar(p, null);
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_13);
    checkInt(
      program,
      callFunction(program, 'foo', [], [TypedValue.box(new PtrType(externalOrigin, 'device', program.intrinsics.int32!), new EPtr(buffer, 0))]),
      WSL_13
    );
  });

  tests.set('passNullAndNotNullFullPoly', () => {
    let program = doPrep(`
          T bar<T>(T p, T)
          {
              return p;
          }
          int foo(device int* p)
          {
              return *bar(p, null);
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_13);
    checkInt(
      program,
      callFunction(program, 'foo', [], [TypedValue.box(new PtrType(externalOrigin, 'device', program.intrinsics.int32!), new EPtr(buffer, 0))]),
      WSL_13
    );
  });

  tests.set('passNullAndNotNullFullPolyReverse', () => {
    let program = doPrep(`
          T bar<T>(T, T p)
          {
              return p;
          }
          int foo(device int* p)
          {
              return *bar(null, p);
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_13);
    checkInt(
      program,
      callFunction(program, 'foo', [], [TypedValue.box(new PtrType(externalOrigin, 'device', program.intrinsics.int32!), new EPtr(buffer, 0))]),
      WSL_13
    );
  });

  tests.set('nullTypeVariableUnify', () => {
    let left: NullType = new NullType(externalOrigin);
    let right: TypeVariable = new TypeVariable(externalOrigin, 'T', null);
    if (left.equals(right)) {
      throw new Error(`Should not be equal but are: ${left} and ${right}`);
    }
    if (right.equals(left)) {
      throw new Error(`Should not be equal but are: ${left} and ${right}`);
    }
    const everyOrder = (array: string[][], callback: (order: string[][]) => void): void => {
      const recurse = (array: string[][], callback: (order: string[][]) => void, order: string[][]): void => {
        if (!array.length) {
          return callback(order);
        }

        for (let i = 0; i < array.length; ++i) {
          let nextArray = array.concat();
          nextArray.splice(i, 1);
          return recurse(nextArray, callback, order.concat([array[i]]));
        }
      };
      return recurse(array, callback, []);
    };

    const everyPair = (things: string[]): string[][] => {
      let result: string[][] = [];
      for (let i = 0; i < things.length; ++i) {
        for (let j = 0; j < things.length; ++j) {
          if (i !== j) {
            result.push([things[i], things[j]]);
          }
        }
      }
      return result;
    };

    everyOrder(everyPair(['nullType', 'variableType', 'ptrType']), (order) => {
      let types: Map<string, NullType | TypeVariable | PtrType> = new Map();
      types.set('nullType', new NullType(externalOrigin));
      types.set('variableType', new TypeVariable(externalOrigin, 'T', null));
      types.set('ptrType', new PtrType(externalOrigin, 'constant', new NativeType(externalOrigin, 'foo_t', [])));
      let unificationContext = new UnificationContext([types.get('variableType') as TypeVariable]);
      for (let orderArr of order) {
        let left: HmType = types.get(orderArr[0]) as HmType;
        let right: HmType = types.get(orderArr[1]) as HmType;
        let result = left.unify(unificationContext, right);
        if (!result) {
          throw new Error('In order ' + order + ' cannot unify ' + left + ' with ' + right);
        }
      }
      if (!unificationContext.verify().result) {
        throw new Error('In order ' + order.map((value) => '(' + value + ')') + ' cannot verify');
      }
    });
  });

  tests.set('doubleNot', () => {
    let program = doPrep(`
          bool foo(bool x)
          {
              return !!x;
          }
      `);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false)]), false);
  });

  tests.set('simpleRecursion', () => {
    checkFail(
      () =>
        doPrep(`
              void foo<T>(T x)
              {
                  foo(&x);
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('protocolMonoSigPolyDef', () => {
    let program = doPrep(`
          struct IntAnd<T> {
              int first;
              T second;
          }
          IntAnd<T> intAnd<T>(int first, T second)
          {
              IntAnd<T> result;
              result.first = first;
              result.second = second;
              return result;
          }
          protocol IntAndable {
              IntAnd<int> intAnd(IntAndable, int);
          }
          int foo<T:IntAndable>(T first, int second)
          {
              IntAnd<int> result = intAnd(first, second);
              return result.first + result.second;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_54), makeInt(program, WSL_12)]), WSL_54 + WSL_12);
  });

  tests.set('protocolPolySigPolyDef', () => {
    let program = doPrep(`
          struct IntAnd<T> {
              int first;
              T second;
          }
          IntAnd<T> intAnd<T>(int first, T second)
          {
              IntAnd<T> result;
              result.first = first;
              result.second = second;
              return result;
          }
          protocol IntAndable {
              IntAnd<T> intAnd<T>(IntAndable, T);
          }
          int foo<T:IntAndable>(T first, int second)
          {
              IntAnd<int> result = intAnd(first, second);
              return result.first + result.second;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_54), makeInt(program, WSL_12)]), WSL_54 + WSL_12);
  });

  tests.set('protocolDoublePolySigDoublePolyDef', () => {
    let program = doPrep(`
          struct IntAnd<T, U> {
              int first;
              T second;
              U third;
          }
          IntAnd<T, U> intAnd<T, U>(int first, T second, U third)
          {
              IntAnd<T, U> result;
              result.first = first;
              result.second = second;
              result.third = third;
              return result;
          }
          protocol IntAndable {
              IntAnd<T, U> intAnd<T, U>(IntAndable, T, U);
          }
          int foo<T:IntAndable>(T first, int second, int third)
          {
              IntAnd<int, int> result = intAnd(first, second, third);
              return result.first + result.second + result.third;
          }
      `);
    checkInt(
      program,
      callFunction(program, 'foo', [], [makeInt(program, WSL_54), makeInt(program, WSL_12), makeInt(program, WSL_39)]),
      WSL_54 + WSL_12 + WSL_39
    );
  });

  tests.set('protocolDoublePolySigDoublePolyDefExplicit', () => {
    let program = doPrep(`
          struct IntAnd<T, U> {
              int first;
              T second;
              U third;
          }
          IntAnd<T, U> intAnd<T, U>(int first, T second, U third)
          {
              IntAnd<T, U> result;
              result.first = first;
              result.second = second;
              result.third = third;
              return result;
          }
          protocol IntAndable {
              IntAnd<T, U> intAnd<T, U>(IntAndable, T, U);
          }
          int foo<T:IntAndable>(T first, int second, int third)
          {
              IntAnd<int, int> result = intAnd<int, int>(first, second, third);
              return result.first + result.second + result.third;
          }
      `);
    checkInt(
      program,
      callFunction(program, 'foo', [], [makeInt(program, WSL_54), makeInt(program, WSL_12), makeInt(program, WSL_39)]),
      WSL_54 + WSL_12 + WSL_39
    );
  });

  tests.set('variableShadowing', () => {
    let program = doPrep(`
          int foo()
          {
              int y;
              int x = 7;
              {
                  int x = 8;
                  y = x;
              }
              return y;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_8);
    program = doPrep(`
          int foo()
          {
              int y;
              int x = 7;
              {
                  int x = 8;
              }
              y = x;
              return y;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_7);
  });

  tests.set('ifStatement', () => {
    let program = doPrep(`
          int foo(int x)
          {
              int y = 6;
              if (x == 7) {
                  y = 8;
              }
              return y;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_8);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_8)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_9)]), WSL_6);
  });

  tests.set('ifElseStatement', () => {
    let program = doPrep(`
          int foo(int x)
          {
              int y = 6;
              if (x == 7) {
                  y = 8;
              } else {
                  y = 9;
              }
              return y;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_9);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_9);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_9);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_9);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_8);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_8)]), WSL_9);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_9)]), WSL_9);
  });

  tests.set('ifElseIfStatement', () => {
    let program = doPrep(`
          int foo(int x)
          {
              int y = 6;
              if (x == 7) {
                  y = 8;
              } else if (x == 8) {
                  y = 9;
              }
              return y;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_8);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_8)]), WSL_9);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_9)]), WSL_6);
  });

  tests.set('ifElseIfElseStatement', () => {
    let program = doPrep(`
          int foo(int x)
          {
              int y = 6;
              if (x == 7) {
                  y = 8;
              } else if (x == 8) {
                  y = 9;
              } else {
                  y = 10;
              }
              return y;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_8);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_8)]), WSL_9);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_9)]), WSL_10);
  });

  tests.set('returnIf', () => {
    checkFail(
      () =>
        doPrep(`
              int foo(int x)
              {
                  int y = 6;
                  if (x == 7) {
                      return y;
                  }
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              int foo(int x)
              {
                  int y = 6;
                  if (x == 7) {
                      return y;
                  } else {
                      y = 8;
                  }
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              int foo(int x)
              {
                  int y = 6;
                  if (x == 7) {
                      y = 8;
                  } else {
                      return y;
                  }
              }
          `),
      (e) => e instanceof WTypeError
    );
    let program = doPrep(`
          int foo(int x)
          {
              int y = 6;
              if (x == 7) {
                  return 8;
              } else {
                  return 10;
              }
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_8);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_8)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_9)]), WSL_10);
    checkFail(
      () =>
        doPrep(`
              int foo(int x)
              {
                  int y = 6;
                  if (x == 7) {
                      return 8;
                  } else if (x == 9) {
                      return 10;
                  }
              }
          `),
      (e) => e instanceof WTypeError
    );
    program = doPrep(`
          int foo(int x)
          {
              int y = 6;
              if (x == 7) {
                  return 8;
              } else {
                  y = 9;
              }
              return y;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_9);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_9);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_9);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_9);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_8);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_8)]), WSL_9);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_9)]), WSL_9);
    checkFail(
      () =>
        doPrep(`
              int foo(int x)
              {
                  int y = 6;
                  if (x == 7) {
                      return 8;
                  } else {
                      return 10;
                  }
                  return 11;
              }
          `),
      (e) => e instanceof WTypeError
    );
    program = doPrep(`
          int foo(int x)
          {
              int y = 6;
              if (x == 7)
                  int y = 8;
              return y;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_6);
  });

  tests.set('simpleWhile', () => {
    let program = doPrep(`
          int foo(int x)
          {
              while (x < 13)
                  x = x * 2;
              return x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1)]), WSL_16);
  });

  tests.set('protocolMonoPolySigDoublePolyDefExplicit', () => {
    checkFail(
      () => {
        let program = doPrep(`
                  struct IntAnd<T, U> {
                      int first;
                      T second;
                      U third;
                  }
                  IntAnd<T, U> intAnd<T, U>(int first, T second, U third)
                  {
                      IntAnd<T, U> result;
                      result.first = first;
                      result.second = second;
                      result.third = third;
                      return result;
                  }
                  protocol IntAndable {
                      IntAnd<T, int> intAnd<T>(IntAndable, T, int);
                  }
                  int foo<T:IntAndable>(T first, int second, int third)
                  {
                      IntAnd<int, int> result = intAnd<int>(first, second, third);
                      return result.first + result.second + result.third;
                  }
              `);
        return callFunction(program, 'foo', [], [makeInt(program, WSL_54), makeInt(program, WSL_12), makeInt(program, WSL_39)]);
      },
      (e) => e instanceof WTypeError
    );
  });

  tests.set('ambiguousOverloadSimple', () => {
    checkFail(
      () =>
        doPrep(`
              void foo<T>(int, T) { }
              void foo<T>(T, int) { }
              void bar(int a, int b) { foo(a, b); }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('ambiguousOverloadOverlapping', () => {
    checkFail(
      () =>
        doPrep(`
              void foo<T>(int, T) { }
              void foo<T>(T, T) { }
              void bar(int a, int b) { foo(a, b); }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('ambiguousOverloadTieBreak', () => {
    doPrep(`
          void foo<T>(int, T) { }
          void foo<T>(T, T) { }
          void foo(int, int) { }
          void bar(int a, int b) { foo(a, b); }
      `);
  });

  tests.set('intOverloadResolution', () => {
    let program = doPrep(`
          int foo(int) { return 1; }
          int foo(uint) { return 2; }
          int foo(double) { return 3; }
          int bar() { return foo(42); }
      `);
    checkInt(program, callFunction(program, 'bar', [], []), 1);
  });

  tests.set('intOverloadResolutionReverseOrder', () => {
    let program = doPrep(`
          int foo(double) { return 3; }
          int foo(uint) { return 2; }
          int foo(int) { return 1; }
          int bar() { return foo(42); }
      `);
    checkInt(program, callFunction(program, 'bar', [], []), 1);
  });

  tests.set('intOverloadResolutionGeneric', () => {
    let program = doPrep(`
          int foo(int) { return 1; }
          int foo<T>(T) { return 2; }
          int bar() { return foo(42); }
      `);
    checkInt(program, callFunction(program, 'bar', [], []), 1);
  });

  tests.set('intLiteralGeneric', () => {
    let program = doPrep(`
          int foo<T>(T x) { return 3478; }
          int bar() { return foo(42); }
      `);
    checkInt(program, callFunction(program, 'bar', [], []), WSL_3478);
  });

  tests.set('intLiteralGenericWithProtocols', () => {
    let program = doPrep(`
          protocol MyConvertibleToInt {
              operator int(MyConvertibleToInt);
          }
          int foo<T:MyConvertibleToInt>(T x) { return int(x); }
          int bar() { return foo(42); }
      `);
    checkInt(program, callFunction(program, 'bar', [], []), WSL_42);
  });

  tests.set('uintLiteralGeneric', () => {
    let program = doPrep(`
          int foo<T>(T x) { return 3478; }
          int bar() { return foo(42u); }
      `);
    checkInt(program, callFunction(program, 'bar', [], []), WSL_3478);
  });

  tests.set('uintLiteralGenericWithProtocols', () => {
    let program = doPrep(`
          protocol MyConvertibleToUint {
              operator uint(MyConvertibleToUint);
          }
          uint foo<T:MyConvertibleToUint>(T x) { return uint(x); }
          uint bar() { return foo(42u); }
      `);
    checkUint(program, callFunction(program, 'bar', [], []), WSL_42);
  });

  tests.set('intLiteralGenericSpecific', () => {
    let program = doPrep(`
          T foo<T>(T x) { return x; }
          int bar() { return foo(int(42)); }
      `);
    checkInt(program, callFunction(program, 'bar', [], []), WSL_42);
  });

  tests.set('simpleConstexpr', () => {
    let program = doPrep(`
          int foo<int a>(int b)
          {
              return a + b;
          }
          int bar(int b)
          {
              return foo<42>(b);
          }
      `);
    checkInt(program, callFunction(program, 'bar', [], [makeInt(program, WSL_58)]), WSL_58 + WSL_42);
  });

  tests.set('break', () => {
    let program = doPrep(`
          int foo(int x)
          {
              while (true) {
                  x = x * 2;
                  if (x >= 7)
                      break;
              }
              return x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1)]), WSL_8);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_10)]), WSL_20);
    program = doPrep(`
          int foo(int x)
          {
              while (true) {
                  while (true) {
                      x = x * 2;
                      if (x >= 7)
                          break;
                  }
                  x = x - 1;
                  break;
              }
              return x;

          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1)]), WSL_7);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_10)]), WSL_19);
    checkFail(
      () =>
        doPrep(`
              int foo(int x)
              {
                  while (true) {
                      {
                          break;
                      }
                      x = x + 1;
                  }
                  return x;
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              int foo(int x)
              {
                  break;
                  return x;
              }
          `),
      (e) => e instanceof WTypeError
    );
    program = doPrep(`
              int foo(int x)
              {
                  while (true) {
                      if (x == 7) {
                          break;
                      }
                      x = x + 1;
                  }
                  return x;
              }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1)]), WSL_7);
    program = doPrep(`
              int foo(int x)
              {
                  while (true) {
                      break;
                  }
                  return x;
              }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1)]), 1);
    program = doPrep(`
              int foo()
              {
                  while (true) {
                      return 7;
                  }
              }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_7);
    checkFail(
      () =>
        doPrep(`
              int foo(int x)
              {
                  while(true) {
                      break;
                      return 7;
                  }
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('continue', () => {
    let program = doPrep(`
          int foo(int x)
          {
              while (x < 10) {
                  if (x == 8) {
                      x = x + 1;
                      continue;
                  }
                  x = x * 2;
              }
              return x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1)]), WSL_18);
    checkFail(
      () =>
        doPrep(`
              int foo(int x)
              {
                  continue;
                  return x;

              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('doWhile', () => {
    let program = doPrep(`
          int foo(int x)
          {
              int y = 7;
              do {
                  y = 8;
                  break;
              } while (x < 10);
              return y;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1)]), WSL_8);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_11)]), WSL_8);
    program = doPrep(`
          int foo(int x)
          {
              int y = 7;
              do {
                  y = 8;
                  break;
              } while (y == 7);
              return y;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1)]), WSL_8);
    program = doPrep(`
          int foo(int x)
          {
              int sum = 0;
              do {
                  if (x == 11) {
                      x = 15;
                      continue;
                  }
                  sum = sum + x;
                  x = x + 1;
              } while (x < 13);
              return sum;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_9)]), WSL_19);
  });

  tests.set('forLoop', () => {
    let program = doPrep(`
          int foo(int x)
          {
              int sum = 0;
              int i;
              for (i = 0; i < x; i = i + 1) {
                  sum = sum + i;
              }
              return sum;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_3);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_10);
    program = doPrep(`
          int foo(int x)
          {
              int sum = 0;
              for (int i = 0; i < x; i = i + 1) {
                  sum = sum + i;
              }
              return sum;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_3);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_10);
    program = doPrep(`
          int foo(int x)
          {
              int sum = 0;
              int i = 100;
              for (int i = 0; i < x; i = i + 1) {
                  sum = sum + i;
              }
              return sum;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_3);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_10);
    program = doPrep(`
          int foo(int x)
          {
              int sum = 0;
              for (int i = 0; i < x; i = i + 1) {
                  if (i == 4)
                      continue;
                  sum = sum + i;
              }
              return sum;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_3);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_11);
    program = doPrep(`
          int foo(int x)
          {
              int sum = 0;
              for (int i = 0; i < x; i = i + 1) {
                  if (i == 5)
                      break;
                  sum = sum + i;
              }
              return sum;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_3);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_10);
    program = doPrep(`
          int foo(int x)
          {
              int sum = 0;
              for (int i = 0; ; i = i + 1) {
                  if (i >= x)
                      break;
                  sum = sum + i;
              }
              return sum;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_3);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_15);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_21);
    program = doPrep(`
          int foo(int x)
          {
              int sum = 0;
              int i = 0;
              for ( ; ; i = i + 1) {
                  if (i >= x)
                      break;
                  sum = sum + i;
              }
              return sum;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_3);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_15);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_21);
    program = doPrep(`
          int foo(int x)
          {
              int sum = 0;
              int i = 0;
              for ( ; ; ) {
                  if (i >= x)
                      break;
                  sum = sum + i;
                  i = i + 1;
              }
              return sum;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_3);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_10);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_15);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_21);
    checkFail(
      () =>
        doPrep(`
              void foo(int x)
              {
                  for (int i = 0; ; i = i + 1) {
                      break;
                      x = i;
                  }
              }
          `),
      (e) => e instanceof WTypeError
    );
    program = doPrep(`
          int foo(int x)
          {
              for ( ; ; ) {
                  return 7;
              }
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_7);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_7);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_7);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_7);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_7);
    checkFail(
      () =>
        doPrep(`
              int foo(int x)
              {
                  for ( ; x < 10; ) {
                      return 7;
                  }
              }
          `),
      (e) => e instanceof WTypeError
    );
    program = doPrep(`
          int foo(int x)
          {
              for ( ; true; ) {
                  return 7;
              }
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_3)]), WSL_7);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_4)]), WSL_7);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_5)]), WSL_7);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_6)]), WSL_7);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_7)]), WSL_7);
  });

  tests.set('chainConstexpr', () => {
    let program = doPrep(`
          int foo<int a>(int b)
          {
              return a + b;
          }
          int bar<int a>(int b)
          {
              return foo<a>(b);
          }
          int baz(int b)
          {
              return bar<42>(b);
          }
      `);
    checkInt(program, callFunction(program, 'baz', [], [makeInt(program, WSL_58)]), WSL_58 + WSL_42);
  });

  tests.set('chainGeneric', () => {
    let program = doPrep(`
          T foo<T>(T x)
          {
              return x;
          }
          T bar<T>(thread T* ptr)
          {
              return *foo(ptr);
          }
          int baz(int x)
          {
              return bar(&x);
          }
      `);
    checkInt(program, callFunction(program, 'baz', [], [makeInt(program, WSL_37)]), WSL_37);
  });

  tests.set('chainStruct', () => {
    let program = doPrep(`
          struct Foo<T> {
              T f;
          }
          struct Bar<T> {
              Foo<thread T*> f;
          }
          int foo(thread Bar<int>* x)
          {
              return *x->f.f;
          }
          int bar(int a)
          {
              Bar<int> x;
              x.f.f = &a;
              return foo(&x);
          }
      `);
    checkInt(program, callFunction(program, 'bar', [], [makeInt(program, WSL_4657)]), WSL_4657);
  });

  tests.set('chainStructNewlyValid', () => {
    let program = doPrep(`
          struct Foo<T> {
              T f;
          }
          struct Bar<T> {
              Foo<device T*> f;
          }
          int foo(thread Bar<int>* x)
          {
              return *x->f.f;
          }
          int bar(device int* a)
          {
              Bar<int> x;
              x.f.f = a;
              return foo(&x);
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_78453);
    checkInt(
      program,
      callFunction(program, 'bar', [], [TypedValue.box(new PtrType(externalOrigin, 'device', program.intrinsics.int32!), new EPtr(buffer, 0))]),
      WSL_78453
    );
  });

  tests.set('chainStructDevice', () => {
    let program = doPrep(`
          struct Foo<T> {
              T f;
          }
          struct Bar<T> {
              Foo<device T*> f;
          }
          int foo(thread Bar<int>* x)
          {
              return *x->f.f;
          }
          int bar(device int* a)
          {
              Bar<int> x;
              x.f.f = a;
              return foo(&x);
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_79201);
    checkInt(
      program,
      callFunction(program, 'bar', [], [TypedValue.box(new PtrType(externalOrigin, 'device', program.intrinsics.int32!), new EPtr(buffer, 0))]),
      WSL_79201
    );
  });

  tests.set('paramChainStructDevice', () => {
    let program = doPrep(`
          struct Foo<T> {
              T f;
          }
          struct Bar<T> {
              Foo<T> f;
          }
          int foo(thread Bar<device int*>* x)
          {
              return *x->f.f;
          }
          int bar(device int* a)
          {
              Bar<device int*> x;
              x.f.f = a;
              return foo(&x);
          }
      `);
    let buffer = new EBuffer(1);
    buffer.eBufferSet(0, WSL_79201);
    checkInt(
      program,
      callFunction(program, 'bar', [], [TypedValue.box(new PtrType(externalOrigin, 'device', program.intrinsics.int32!), new EPtr(buffer, 0))]),
      WSL_79201
    );
  });

  tests.set('simpleProtocolExtends', () => {
    let program = doPrep(`
          protocol Foo {
              void foo(thread Foo*);
          }
          protocol Bar : Foo {
              void bar(thread Bar*);
          }
          void fuzz<T:Foo>(thread T* p)
          {
              foo(p);
          }
          void buzz<T:Bar>(thread T* p)
          {
              fuzz(p);
              bar(p);
          }
          void foo(thread int* p)
          {
              *p = *p + 743;
          }
          void bar(thread int* p)
          {
              *p = *p + 91;
          }
          int thingy(int a)
          {
              buzz(&a);
              return a;
          }
      `);
    checkInt(program, callFunction(program, 'thingy', [], [makeInt(program, WSL_642)]), WSL_642 + WSL_743 + WSL_91);
  });

  tests.set('protocolExtendsTwo', () => {
    let program = doPrep(`
          protocol Foo {
              void foo(thread Foo*);
          }
          protocol Bar {
              void bar(thread Bar*);
          }
          protocol Baz : Foo, Bar {
              void baz(thread Baz*);
          }
          void fuzz<T:Foo>(thread T* p)
          {
              foo(p);
          }
          void buzz<T:Bar>(thread T* p)
          {
              bar(p);
          }
          void xuzz<T:Baz>(thread T* p)
          {
              fuzz(p);
              buzz(p);
              baz(p);
          }
          void foo(thread int* p)
          {
              *p = *p + 743;
          }
          void bar(thread int* p)
          {
              *p = *p + 91;
          }
          void baz(thread int* p)
          {
              *p = *p + 39;
          }
          int thingy(int a)
          {
              xuzz(&a);
              return a;
          }
      `);
    checkInt(program, callFunction(program, 'thingy', [], [makeInt(program, WSL_642)]), WSL_642 + WSL_743 + WSL_91 + WSL_39);
  });

  tests.set('prefixPlusPlus', () => {
    let program = doPrep(`
          int foo(int x)
          {
              ++x;
              return x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_64)]), WSL_65);
  });

  tests.set('prefixPlusPlusResult', () => {
    let program = doPrep(`
          int foo(int x)
          {
              return ++x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_64)]), WSL_65);
  });

  tests.set('postfixPlusPlus', () => {
    let program = doPrep(`
          int foo(int x)
          {
              x++;
              return x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_64)]), WSL_65);
  });

  tests.set('postfixPlusPlusResult', () => {
    let program = doPrep(`
          int foo(int x)
          {
              return x++;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_64)]), WSL_64);
  });

  tests.set('prefixMinusMinus', () => {
    let program = doPrep(`
          int foo(int x)
          {
              --x;
              return x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_64)]), WSL_63);
  });

  tests.set('prefixMinusMinusResult', () => {
    let program = doPrep(`
          int foo(int x)
          {
              return --x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_64)]), WSL_63);
  });

  tests.set('postfixMinusMinus', () => {
    let program = doPrep(`
          int foo(int x)
          {
              x--;
              return x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_64)]), WSL_63);
  });

  tests.set('postfixMinusMinusResult', () => {
    let program = doPrep(`
          int foo(int x)
          {
              return x--;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_64)]), WSL_64);
  });

  tests.set('plusEquals', () => {
    let program = doPrep(`
          int foo(int x)
          {
              x += 42;
              return x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_385)]), WSL_385 + WSL_42);
  });

  tests.set('plusEqualsResult', () => {
    let program = doPrep(`
          int foo(int x)
          {
              return x += 42;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_385)]), WSL_385 + WSL_42);
  });

  tests.set('minusEquals', () => {
    let program = doPrep(`
          int foo(int x)
          {
              x -= 42;
              return x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_385)]), WSL_385 - WSL_42);
  });

  tests.set('minusEqualsResult', () => {
    let program = doPrep(`
          int foo(int x)
          {
              return x -= 42;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_385)]), WSL_385 - WSL_42);
  });

  tests.set('timesEquals', () => {
    let program = doPrep(`
          int foo(int x)
          {
              x *= 42;
              return x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_385)]), WSL_385 * WSL_42);
  });

  tests.set('timesEqualsResult', () => {
    let program = doPrep(`
          int foo(int x)
          {
              return x *= 42;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_385)]), WSL_385 * WSL_42);
  });

  tests.set('divideEquals', () => {
    let program = doPrep(`
          int foo(int x)
          {
              x /= 42;
              return x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_385)]), (WSL_385 / WSL_42) | 0);
  });

  tests.set('divideEqualsResult', () => {
    let program = doPrep(`
          int foo(int x)
          {
              return x /= 42;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_385)]), (WSL_385 / WSL_42) | 0);
  });

  tests.set('twoIntLiterals', () => {
    let program = doPrep(`
          bool foo()
          {
              return 42 == 42;
          }
      `);
    checkBool(program, callFunction(program, 'foo', [], []), true);
  });

  tests.set('unifyDifferentLiterals', () => {
    checkFail(
      () =>
        doPrep(`
              void bar<T>(T, T)
              {
              }
              void foo()
              {
                  bar(42, 42u);
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('unifyDifferentLiteralsBackwards', () => {
    checkFail(
      () =>
        doPrep(`
              void bar<T>(T, T)
              {
              }
              void foo()
              {
                  bar(42u, 42);
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('unifyVeryDifferentLiterals', () => {
    checkFail(
      () =>
        doPrep(`
              void bar<T>(T, T)
              {
              }
              void foo()
              {
                  bar(42, null);
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('unifyVeryDifferentLiteralsBackwards', () => {
    checkFail(
      () =>
        doPrep(`
              void bar<T>(T, T)
              {
              }
              void foo()
              {
                  bar(null, 42);
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('assignUintToInt', () => {
    checkFail(
      () =>
        doPrep(`
              void foo()
              {
                  int x = 42u;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Type mismatch in variable initialization') !== -1
    );
  });

  tests.set('buildArrayThenSumIt', () => {
    let program = doPrep(`
          int foo()
          {
              int[42] array;
              for (uint i = 0; i < 42; i = i + 1)
                  array[i] = int(i + 5);
              int result;
              for (uint i = 0; i < 42; i = i + 1)
                  result = result + array[i];
              return result;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_42 * WSL_5 + (WSL_42 * WSL_41) / WSL_2);
  });

  tests.set('buildArrayThenSumItUsingArrayReference', () => {
    let program = doPrep(`
          int bar(thread int[] array)
          {
              for (uint i = 0; i < 42; i = i + 1)
                  array[i] = int(i + 5);
              int result;
              for (uint i = 0; i < 42; i = i + 1)
                  result = result + array[i];
              return result;
          }
          int foo()
          {
              int[42] array;
              return bar(@array);
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_42 * WSL_5 + (WSL_42 * WSL_41) / WSL_2);
  });

  tests.set('overrideSubscriptStruct', () => {
    let program = doPrep(`
          struct Foo {
              int x;
              int y;
          }
          thread int* operator&[](thread Foo* foo, uint index)
          {
              if (index == 0)
                  return &foo->x;
              if (index == 1)
                  return &foo->y;
              return null;
          }
          int foo()
          {
              Foo foo;
              foo.x = 498;
              foo.y = 19;
              return foo[0] + foo[1] * 3;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_498 + WSL_19 * WSL_3);
  });

  tests.set('overrideSubscriptStructAndDoStores', () => {
    let program = doPrep(`
          struct Foo {
              int x;
              int y;
          }
          thread int* operator&[](thread Foo* foo, uint index)
          {
              if (index == 0)
                  return &foo->x;
              if (index == 1)
                  return &foo->y;
              return null;
          }
          int foo()
          {
              Foo foo;
              foo[0] = 498;
              foo[1] = 19;
              return foo.x + foo.y;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_498 + WSL_19);
  });

  tests.set('overrideSubscriptStructAndUsePointers', () => {
    let program = doPrep(`
          struct Foo {
              int x;
              int y;
          }
          thread int* operator&[](thread Foo* foo, uint index)
          {
              if (index == 0)
                  return &foo->x;
              if (index == 1)
                  return &foo->y;
              return null;
          }
          int bar(thread Foo* foo)
          {
              return (*foo)[0] + (*foo)[1];
          }
          int foo()
          {
              Foo foo;
              foo.x = 498;
              foo.y = 19;
              return bar(&foo);
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_498 + WSL_19);
  });

  tests.set('overrideSubscriptStructAndUsePointersIncorrectly', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  int x;
                  int y;
              }
              thread int* operator&[](thread Foo* foo, uint index)
              {
                  if (index == 0)
                      return &foo->x;
                  if (index == 1)
                      return &foo->y;
                  return null;
              }
              int bar(thread Foo* foo)
              {
                  return foo[0] + foo[1];
              }
              int foo()
              {
                  Foo foo;
                  foo.x = 498;
                  foo.y = 19;
                  return bar(&foo);
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('makeArrayRefFromLocal', () => {
    let program = doPrep(`
          int bar(thread int[] ref)
          {
              return ref[0];
          }
          int foo()
          {
              int x = 48;
              return bar(@x);
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_48);
  });

  tests.set('makeArrayRefFromPointer', () => {
    let program = doPrep(`
          int bar(thread int[] ref)
          {
              return ref[0];
          }
          int baz(thread int* ptr)
          {
              return bar(@ptr);
          }
          int foo()
          {
              int x = 48;
              return baz(&x);
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_48);
  });

  tests.set('makeArrayRefFromArrayRef', () => {
    checkFail(
      () =>
        doPrep(`
              int bar(thread int[] ref)
              {
                  return ref[0];
              }
              int baz(thread int[] ptr)
              {
                  return bar(@ptr);
              }
              int foo()
              {
                  int x = 48;
                  return baz(@x);
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('simpleLength', () => {
    let program = doPrep(`
          uint foo()
          {
              double[754] array;
              return (@array).length;
          }
      `);
    checkUint(program, callFunction(program, 'foo', [], []), WSL_754);
  });

  tests.set('nonArrayRefArrayLengthSucceed', () => {
    let program = doPrep(`
          uint foo()
          {
              double[754] array;
              return array.length;
          }
      `);
    checkUint(program, callFunction(program, 'foo', [], []), WSL_754);
  });

  tests.set('nonArrayRefArrayLengthFail', () => {
    checkFail(
      () =>
        doPrep(`
              thread uint* lengthPtr()
              {
                  int[42] array;
                  return &(array.length);
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('constexprIsNotLValuePtr', () => {
    checkFail(
      () =>
        doPrep(`
              thread int* foo<int x>()
              {
                  return &x;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('constexprIsNotLValueAssign', () => {
    checkFail(
      () =>
        doPrep(`
              void foo<int x>()
              {
                  x = 42;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('constexprIsNotLValueRMW', () => {
    checkFail(
      () =>
        doPrep(`
              void foo<int x>()
              {
                  x += 42;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('assignLength', () => {
    checkFail(
      () =>
        doPrep(`
              void foo()
              {
                  double[754] array;
                  (@array).length = 42;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Have neither ander nor setter') !== -1
    );
  });

  tests.set('assignLengthHelper', () => {
    checkFail(
      () =>
        doPrep(`
              void bar(thread double[] array)
              {
                  array.length = 42;
              }
              void foo()
              {
                  double[754] array;
                  bar(@array);
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Have neither ander nor setter') !== -1
    );
  });

  tests.set('simpleGetter', () => {
    let program = doPrep(`
          struct Foo {
              int x;
          }
          int operator.y(Foo foo)
          {
              return foo.x;
          }
          int foo()
          {
              Foo foo;
              foo.x = 7804;
              return foo.y;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_7804);
  });

  tests.set('simpleSetter', () => {
    let program = doPrep(`
          struct Foo {
              int x;
          }
          int operator.y(Foo foo)
          {
              return foo.x;
          }
          Foo operator.y=(Foo foo, int value)
          {
              foo.x = value;
              return foo;
          }
          int foo()
          {
              Foo foo;
              foo.y = 7804;
              return foo.x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_7804);
  });

  tests.set('genericAccessors', () => {
    let program = doPrep(`
          struct Foo<T> {
              T x;
              T[3] y;
          }
          struct Bar<T> {
              T x;
              T y;
          }
          Bar<T> operator.z<T>(Foo<T> foo)
          {
              Bar<T> result;
              result.x = foo.x;
              result.y = foo.y[1];
              return result;
          }
          Foo<T> operator.z=<T>(Foo<T> foo, Bar<T> bar)
          {
              foo.x = bar.x;
              foo.y[1] = bar.y;
              return foo;
          }
          T operator.sum<T:Addable>(Foo<T> foo)
          {
              return foo.x + foo.y[0] + foo.y[1] + foo.y[2];
          }
          T operator.sum<T:Addable>(Bar<T> bar)
          {
              return bar.x + bar.y;
          }
          operator<T> Bar<T>(T x, T y)
          {
              Bar<T> result;
              result.x = x;
              result.y = y;
              return result;
          }
          void setup(thread Foo<int>* foo)
          {
              foo->x = 1;
              foo->y[0] = 2;
              foo->y[1] = 3;
              foo->y[2] = 4;
          }
          int testSuperBasic()
          {
              Foo<int> foo;
              setup(&foo);
              return foo.sum;
          }
          int testZSetterDidSetY()
          {
              Foo<int> foo;
              foo.z = Bar<int>(53, 932);
              return foo.y[1];
          }
          int testZSetter()
          {
              Foo<int> foo;
              foo.z = Bar<int>(53, 932);
              return foo.sum;
          }
          int testZGetter()
          {
              Foo<int> foo;
              // This deliberately does not call setup() just so we test this syntax.
              foo.x = 1;
              foo.y[0] = 2;
              foo.y[1] = 3;
              foo.y[2] = 4;
              return foo.z.sum;
          }
          int testLValueEmulation()
          {
              Foo<int> foo;
              setup(&foo);
              foo.z.y *= 5;
              return foo.sum;
          }
      `);
    checkInt(program, callFunction(program, 'testSuperBasic', [], []), 1 + WSL_2 + WSL_3 + WSL_4);
    checkInt(program, callFunction(program, 'testZSetterDidSetY', [], []), WSL_932);
    checkInt(program, callFunction(program, 'testZSetter', [], []), WSL_53 + WSL_932);
    checkInt(program, callFunction(program, 'testZGetter', [], []), 1 + WSL_3);
    checkInt(program, callFunction(program, 'testLValueEmulation', [], []), 1 + WSL_2 + WSL_3 * WSL_5 + WSL_4);
  });

  tests.set('bitSubscriptAccessor', () => {
    let program = doPrep(`
          protocol MyBitmaskable : Equatable {
              MyBitmaskable operator&(MyBitmaskable, MyBitmaskable);
              MyBitmaskable operator|(MyBitmaskable, MyBitmaskable);
              MyBitmaskable operator~(MyBitmaskable);
              MyBitmaskable operator<<(MyBitmaskable, uint);
              MyBitmaskable operator>>(MyBitmaskable, uint);
              operator MyBitmaskable(int);
          }
          T maskForBitIndex<T:MyBitmaskable>(uint index)
          {
              return T(1) << index;
          }
          bool operator[]<T:MyBitmaskable>(T value, uint index)
          {
              return bool(value & maskForBitIndex<T>(index));
          }
          T operator[]=<T:MyBitmaskable>(T value, uint index, bool bit)
          {
              T mask = maskForBitIndex<T>(index);
              if (bit)
                  value |= mask;
              else
                  value &= ~mask;
              return value;
          }
          uint operator.length(int)
          {
              return 32;
          }
          uint operator.length(uint)
          {
              return 32;
          }
          int testIntSetBit3()
          {
              int foo;
              foo[3] = true;
              return foo;
          }
          bool testIntSetGetBit5()
          {
              int foo;
              foo[5] = true;
              return foo[5];
          }
          bool testIntGetBit1()
          {
              int foo;
              return foo[1];
          }
          int testUintSumBits()
          {
              int foo = 42;
              int result;
              for (uint i = 0; i < foo.length; ++i) {
                  if (foo[i])
                      result++;
              }
              return result;
          }
          int testUintSwapBits()
          {
              int foo = 42;
              for (uint i = 0; i < foo.length / 2; ++i) {
                  bool tmp = foo[i];
                  foo[i] = foo[foo.length - i - 1];
                  foo[foo.length - i - 1] = tmp;
              }
              return foo;
          }
          struct Foo {
              uint f;
              uint g;
          }
          operator Foo(uint f, uint g)
          {
              Foo result;
              result.f = f;
              result.g = g;
              return result;
          }
          int operator.h(Foo foo)
          {
              return int((foo.f & 0xffff) | ((foo.g & 0xffff) << 16));
          }
          Foo operator.h=(Foo foo, int value)
          {
              foo.f &= ~0xffffu;
              foo.f |= uint(value) & 0xffff;
              foo.g &= ~0xffffu;
              foo.g |= (uint(value) >> 16) & 0xffff;
              return foo;
          }
          int testLValueEmulation()
          {
              Foo foo;
              foo.f = 42;
              foo.g = 37;
              for (uint i = 0; i < foo.h.length; ++i)
                  foo.h[i] ^= true;
              return int(foo.f + foo.g);
          }
          struct Bar {
              Foo a;
              Foo b;
          }
          Foo operator.c(Bar bar)
          {
              return Foo(uint(bar.a.h), uint(bar.b.h));
          }
          Bar operator.c=(Bar bar, Foo foo)
          {
              bar.a.h = int(foo.f);
              bar.b.h = int(foo.g);
              return bar;
          }
          int testCrazyLValueEmulation()
          {
              Bar bar;
              bar.a.f = 1;
              bar.a.g = 2;
              bar.b.f = 3;
              bar.b.g = 4;
              for (uint i = 0; i < bar.c.h.length; i += 2)
                  bar.c.h[i] ^= true;
              return int(bar.a.f + bar.a.g + bar.b.f + bar.b.g);
          }
      `);
    checkInt(program, callFunction(program, 'testIntSetBit3', [], []), WSL_8);
    checkBool(program, callFunction(program, 'testIntSetGetBit5', [], []), true);
    checkBool(program, callFunction(program, 'testIntGetBit1', [], []), false);
    checkInt(program, callFunction(program, 'testUintSumBits', [], []), WSL_3);
    checkInt(program, callFunction(program, 'testUintSwapBits', [], []), WSL_1409286144);
    checkInt(program, callFunction(program, 'testLValueEmulation', [], []), WSL_130991);
    checkInt(program, callFunction(program, 'testCrazyLValueEmulation', [], []), WSL_43696);
  });

  tests.set('nestedSubscriptLValueEmulationSimple', () => {
    let program = doPrep(`
          struct Foo {
              int[7] array;
          }
          int operator[](Foo foo, uint index)
          {
              return foo.array[index];
          }
          Foo operator[]=(Foo foo, uint index, int value)
          {
              foo.array[index] = value;
              return foo;
          }
          uint operator.length(Foo foo)
          {
              return foo.array.length;
          }
          int sum(Foo foo)
          {
              int result = 0;
              for (uint i = foo.length; i--;)
                  result += foo[i];
              return result;
          }
          struct Bar {
              Foo[6] array;
          }
          uint operator.length(Bar bar)
          {
              return bar.array.length;
          }
          Foo operator[](Bar bar, uint index)
          {
              return bar.array[index];
          }
          Bar operator[]=(Bar bar, uint index, Foo value)
          {
              bar.array[index] = value;
              return bar;
          }
          int sum(Bar bar)
          {
              int result = 0;
              for (uint i = bar.length; i--;)
                  result += sum(bar[i]);
              return result;
          }
          struct Baz {
              Bar[5] array;
          }
          Bar operator[](Baz baz, uint index)
          {
              return baz.array[index];
          }
          Baz operator[]=(Baz baz, uint index, Bar value)
          {
              baz.array[index] = value;
              return baz;
          }
          uint operator.length(Baz baz)
          {
              return baz.array.length;
          }
          int sum(Baz baz)
          {
              int result = 0;
              for (uint i = baz.length; i--;)
                  result += sum(baz[i]);
              return result;
          }
          void setValues(thread Baz* baz)
          {
              for (uint i = baz->length; i--;) {
                  for (uint j = (*baz)[i].length; j--;) {
                      for (uint k = (*baz)[i][j].length; k--;)
                          (*baz)[i][j][k] = int(i + j + k);
                  }
              }
          }
          int testSetValuesAndSum()
          {
              Baz baz;
              setValues(&baz);
              return sum(baz);
          }
          int testSetValuesMutateValuesAndSum()
          {
              Baz baz;
              setValues(&baz);
              for (uint i = baz.length; i--;) {
                  for (uint j = baz[i].length; j--;) {
                      for (uint k = baz[i][j].length; k--;)
                          baz[i][j][k] *= int(k);
                  }
              }
              return sum(baz);
          }
      `);
    checkInt(program, callFunction(program, 'testSetValuesAndSum', [], []), WSL_1575);
    checkInt(program, callFunction(program, 'testSetValuesMutateValuesAndSum', [], []), WSL_5565);
  });

  tests.set('nestedSubscriptLValueEmulationGeneric', () => {
    let program = doPrep(`
          struct Foo<T> {
              T[7] array;
          }
          T operator[]<T>(Foo<T> foo, uint index)
          {
              return foo.array[index];
          }
          Foo<T> operator[]=<T>(Foo<T> foo, uint index, T value)
          {
              foo.array[index] = value;
              return foo;
          }
          uint operator.length<T>(Foo<T> foo)
          {
              return foo.array.length;
          }
          protocol MyAddable {
              MyAddable operator+(MyAddable, MyAddable);
          }
          T sum<T:MyAddable>(Foo<T> foo)
          {
              T result;
              for (uint i = foo.length; i--;)
                  result += foo[i];
              return result;
          }
          struct Bar<T> {
              Foo<T>[6] array;
          }
          uint operator.length<T>(Bar<T> bar)
          {
              return bar.array.length;
          }
          Foo<T> operator[]<T>(Bar<T> bar, uint index)
          {
              return bar.array[index];
          }
          Bar<T> operator[]=<T>(Bar<T> bar, uint index, Foo<T> value)
          {
              bar.array[index] = value;
              return bar;
          }
          T sum<T:MyAddable>(Bar<T> bar)
          {
              T result;
              for (uint i = bar.length; i--;)
                  result += sum(bar[i]);
              return result;
          }
          struct Baz<T> {
              Bar<T>[5] array;
          }
          Bar<T> operator[]<T>(Baz<T> baz, uint index)
          {
              return baz.array[index];
          }
          Baz<T> operator[]=<T>(Baz<T> baz, uint index, Bar<T> value)
          {
              baz.array[index] = value;
              return baz;
          }
          uint operator.length<T>(Baz<T> baz)
          {
              return baz.array.length;
          }
          T sum<T:MyAddable>(Baz<T> baz)
          {
              T result;
              for (uint i = baz.length; i--;)
                  result += sum(baz[i]);
              return result;
          }
          protocol MyConvertibleFromUint {
              operator MyConvertibleFromUint(uint);
          }
          protocol SetValuable : MyAddable, MyConvertibleFromUint { }
          void setValues<T:SetValuable>(thread Baz<T>* baz)
          {
              for (uint i = baz->length; i--;) {
                  for (uint j = (*baz)[i].length; j--;) {
                      for (uint k = (*baz)[i][j].length; k--;)
                          (*baz)[i][j][k] = T(i + j + k);
                  }
              }
          }
          int testSetValuesAndSum()
          {
              Baz<int> baz;
              setValues(&baz);
              return sum(baz);
          }
          int testSetValuesMutateValuesAndSum()
          {
              Baz<int> baz;
              setValues(&baz);
              for (uint i = baz.length; i--;) {
                  for (uint j = baz[i].length; j--;) {
                      for (uint k = baz[i][j].length; k--;)
                          baz[i][j][k] *= int(k);
                  }
              }
              return sum(baz);
          }
      `);
    checkInt(program, callFunction(program, 'testSetValuesAndSum', [], []), WSL_1575);
    checkInt(program, callFunction(program, 'testSetValuesMutateValuesAndSum', [], []), WSL_5565);
  });

  tests.set('boolBitAnd', () => {
    let program = doPrep(`
          bool foo(bool a, bool b)
          {
              return a & b;
          }
      `);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false), makeBool(program, false)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true), makeBool(program, false)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false), makeBool(program, true)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true), makeBool(program, true)]), true);
  });

  tests.set('boolBitOr', () => {
    let program = doPrep(`
          bool foo(bool a, bool b)
          {
              return a | b;
          }
      `);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false), makeBool(program, false)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true), makeBool(program, false)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false), makeBool(program, true)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true), makeBool(program, true)]), true);
  });

  tests.set('boolBitXor', () => {
    let program = doPrep(`
          bool foo(bool a, bool b)
          {
              return a ^ b;
          }
      `);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false), makeBool(program, false)]), false);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true), makeBool(program, false)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false), makeBool(program, true)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true), makeBool(program, true)]), false);
  });

  tests.set('boolBitNot', () => {
    let program = doPrep(`
          bool foo(bool a)
          {
              return ~a;
          }
      `);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, false)]), true);
    checkBool(program, callFunction(program, 'foo', [], [makeBool(program, true)]), false);
  });

  tests.set('intBitAnd', () => {
    let program = doPrep(`
          int foo(int a, int b)
          {
              return a & b;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1), makeInt(program, WSL_7)]), 1);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_65535), makeInt(program, WSL_42)]), WSL_42);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, -1), makeInt(program, WSLNEGATIVE_7)]), WSLNEGATIVE_7);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 0), makeInt(program, WSL_85732)]), 0);
  });

  tests.set('intBitOr', () => {
    let program = doPrep(`
          int foo(int a, int b)
          {
              return a | b;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1), makeInt(program, WSL_7)]), WSL_7);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_65535), makeInt(program, WSL_42)]), WSL_65535);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, -1), makeInt(program, WSLNEGATIVE_7)]), -1);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 0), makeInt(program, WSL_85732)]), WSL_85732);
  });

  tests.set('intBitXor', () => {
    let program = doPrep(`
          int foo(int a, int b)
          {
              return a ^ b;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1), makeInt(program, WSL_7)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_65535), makeInt(program, WSL_42)]), WSL_65493);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, -1), makeInt(program, WSLNEGATIVE_7)]), WSL_6);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 0), makeInt(program, WSL_85732)]), WSL_85732);
  });

  tests.set('intBitNot', () => {
    let program = doPrep(`
          int foo(int a)
          {
              return ~a;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1)]), WSLNEGATIVE_2);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_65535)]), WSLNEGATIVE_65536);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, -1)]), 0);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 0)]), -1);
  });

  tests.set('intLShift', () => {
    let program = doPrep(`
          int foo(int a, uint b)
          {
              return a << b;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1), makeUint(program, WSL_7)]), WSL_128);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_65535), makeUint(program, WSL_2)]), WSL_262140);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, -1), makeUint(program, WSL_5)]), WSLNEGATIVE_32);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 0), makeUint(program, WSL_3)]), 0);
  });

  tests.set('intRShift', () => {
    let program = doPrep(`
          int foo(int a, uint b)
          {
              return a >> b;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 1), makeUint(program, WSL_7)]), 0);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_65535), makeUint(program, WSL_2)]), WSL_16383);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, -1), makeUint(program, WSL_5)]), -1);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 0), makeUint(program, WSL_3)]), 0);
  });

  tests.set('uintBitAnd', () => {
    let program = doPrep(`
          uint foo(uint a, uint b)
          {
              return a & b;
          }
      `);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, 1), makeUint(program, WSL_7)]), 1);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, WSL_65535), makeUint(program, WSL_42)]), WSL_42);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, -1), makeUint(program, WSLNEGATIVE_7)]), WSL_4294967289);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, 0), makeUint(program, WSL_85732)]), 0);
  });

  tests.set('uintBitOr', () => {
    let program = doPrep(`
          uint foo(uint a, uint b)
          {
              return a | b;
          }
      `);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, 1), makeUint(program, WSL_7)]), WSL_7);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, WSL_65535), makeUint(program, WSL_42)]), WSL_65535);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, -1), makeUint(program, WSLNEGATIVE_7)]), WSL_4294967295);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, 0), makeUint(program, WSL_85732)]), WSL_85732);
  });

  tests.set('uintBitXor', () => {
    let program = doPrep(`
          uint foo(uint a, uint b)
          {
              return a ^ b;
          }
      `);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, 1), makeUint(program, WSL_7)]), WSL_6);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, WSL_65535), makeUint(program, WSL_42)]), WSL_65493);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, -1), makeUint(program, WSLNEGATIVE_7)]), WSL_6);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, 0), makeUint(program, WSL_85732)]), WSL_85732);
  });

  tests.set('uintBitNot', () => {
    let program = doPrep(`
          uint foo(uint a)
          {
              return ~a;
          }
      `);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, 1)]), WSL_4294967294);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, WSL_65535)]), WSL_4294901760);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, -1)]), 0);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, 0)]), WSL_4294967295);
  });

  tests.set('uintLShift', () => {
    let program = doPrep(`
          uint foo(uint a, uint b)
          {
              return a << b;
          }
      `);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, 1), makeUint(program, WSL_7)]), WSL_128);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, WSL_65535), makeUint(program, WSL_2)]), WSL_262140);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, -1), makeUint(program, WSL_5)]), WSL_4294967264);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, 0), makeUint(program, WSL_3)]), 0);
  });

  tests.set('uintRShift', () => {
    let program = doPrep(`
          uint foo(uint a, uint b)
          {
              return a >> b;
          }
      `);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, 1), makeUint(program, WSL_7)]), 0);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, WSL_65535), makeUint(program, WSL_2)]), WSL_16383);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, -1), makeUint(program, WSL_5)]), WSL_134217727);
    checkUint(program, callFunction(program, 'foo', [], [makeUint(program, 0), makeUint(program, WSL_3)]), 0);
  });

  tests.set('uint8BitAnd', () => {
    let program = doPrep(`
          uint8 foo(uint8 a, uint8 b)
          {
              return a & b;
          }
      `);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, 1), makeUint8(program, WSL_7)]), 1);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_65535), makeUint8(program, WSL_42)]), WSL_42);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, -1), makeUint8(program, WSLNEGATIVE_7)]), WSL_249);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, 0), makeUint8(program, WSL_85732)]), 0);
  });

  tests.set('uint8BitOr', () => {
    let program = doPrep(`
          uint8 foo(uint8 a, uint8 b)
          {
              return a | b;
          }
      `);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, 1), makeUint8(program, WSL_7)]), WSL_7);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_65535), makeUint8(program, WSL_42)]), WSL_255);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, -1), makeUint8(program, WSLNEGATIVE_7)]), WSL_255);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, 0), makeUint8(program, WSL_85732)]), WSL_228);
  });

  tests.set('uint8BitXor', () => {
    let program = doPrep(`
          uint8 foo(uint8 a, uint8 b)
          {
              return a ^ b;
          }
      `);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, 1), makeUint8(program, WSL_7)]), WSL_6);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_65535), makeUint8(program, WSL_42)]), WSL_213);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, -1), makeUint8(program, WSLNEGATIVE_7)]), WSL_6);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, 0), makeUint8(program, WSL_85732)]), WSL_228);
  });

  tests.set('uint8BitNot', () => {
    let program = doPrep(`
          uint8 foo(uint8 a)
          {
              return ~a;
          }
      `);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, 1)]), WSL_254);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_65535)]), 0);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, -1)]), 0);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, 0)]), WSL_255);
  });

  tests.set('uint8LShift', () => {
    let program = doPrep(`
          uint8 foo(uint8 a, uint b)
          {
              return a << b;
          }
      `);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, 1), makeUint(program, WSL_7)]), WSL_128);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_65535), makeUint(program, WSL_2)]), WSL_252);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, -1), makeUint(program, WSL_5)]), WSL_224);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, 0), makeUint(program, WSL_3)]), 0);
  });

  tests.set('uint8RShift', () => {
    let program = doPrep(`
          uint8 foo(uint8 a, uint b)
          {
              return a >> b;
          }
      `);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, 1), makeUint(program, WSL_7)]), 0);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, WSL_65535), makeUint(program, WSL_2)]), WSL_255);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, -1), makeUint(program, WSL_5)]), WSL_255);
    checkUint8(program, callFunction(program, 'foo', [], [makeUint8(program, 0), makeUint(program, WSL_3)]), 0);
  });

  tests.set('floatMath', () => {
    let program = doPrep(`
          bool foo()
          {
              return 42.5 == 42.5;
          }
          bool foo2()
          {
              return 42.5f == 42.5;
          }
          bool foo3()
          {
              return 42.5 == 42.5f;
          }
          bool foo4()
          {
              return 42.5f == 42.5f;
          }
          bool foo5()
          {
              return 42.5d == 42.5d;
          }
          float bar(float x)
          {
              return x;
          }
          float foo6()
          {
              return bar(7.5);
          }
          float foo7()
          {
              return bar(7.5f);
          }
          float foo8()
          {
              return bar(7.5d);
          }
          float foo9()
          {
              return float(7.5);
          }
          float foo10()
          {
              return float(7.5f);
          }
          float foo11()
          {
              return float(7.5d);
          }
          float foo12()
          {
              return float(7);
          }
          float foo13()
          {
              double x = 7.5d;
              return float(x);
          }
          double foo14()
          {
              double x = 7.5f;
              return double(x);
          }
      `);
    checkBool(program, callFunction(program, 'foo', [], []), true);
    checkBool(program, callFunction(program, 'foo2', [], []), true);
    checkBool(program, callFunction(program, 'foo3', [], []), true);
    checkBool(program, callFunction(program, 'foo4', [], []), true);
    checkBool(program, callFunction(program, 'foo5', [], []), true);
    checkFloat(program, callFunction(program, 'foo6', [], []), WSLDECIMAL_7);
    checkFloat(program, callFunction(program, 'foo7', [], []), WSLDECIMAL_7);
    checkFloat(program, callFunction(program, 'foo8', [], []), WSLDECIMAL_7);
    checkFloat(program, callFunction(program, 'foo9', [], []), WSLDECIMAL_7);
    checkFloat(program, callFunction(program, 'foo10', [], []), WSLDECIMAL_7);
    checkFloat(program, callFunction(program, 'foo11', [], []), WSLDECIMAL_7);
    checkFloat(program, callFunction(program, 'foo12', [], []), WSL_7);
    checkFloat(program, callFunction(program, 'foo13', [], []), WSLDECIMAL_7);
    checkDouble(program, callFunction(program, 'foo14', [], []), WSLDECIMAL_7);
    checkFail(
      () =>
        doPrep(`
              int bar(int x)
              {
                  return x;
              }
              int foo()
              {
                  bar(4.);
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              int bar(int x)
              {
                  return x;
              }
              int foo()
              {
                  bar(4.d);
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              int bar(int x)
              {
                  return x;
              }
              int foo()
              {
                  bar(4.f);
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              uint bar(uint x)
              {
                  return x;
              }
              int foo()
              {
                  bar(4.);
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              uint bar(uint x)
              {
                  return x;
              }
              int foo()
              {
                  bar(4.d);
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              uint bar(uint x)
              {
                  return x;
              }
              int foo()
              {
                  bar(4.f);
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              float bar(float x)
              {
                  return x;
              }
              void foo()
              {
                  bar(16777217.d);
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              float bar(float x)
              {
                  return x;
              }
              float foo()
              {
                  double x = 7.;
                  return bar(x);
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              float foo()
              {
                  double x = 7.;
                  return x;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('genericCastInfer', () => {
    let program = doPrep(`
          struct Complex<T> {
              T real;
              T imag;
          }
          operator<T> Complex<T>(T real, T imag)
          {
              Complex<T> result;
              result.real = real;
              result.imag = imag;
              return result;
          }
          int foo()
          {
              Complex<int> x = Complex<int>(1, 2);
              return x.real + x.imag;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_3);
  });

  tests.set('booleanMath', () => {
    let program = doPrep(`
          bool foo()
          {
              return true && true;
          }
          bool foo2()
          {
              return true && false;
          }
          bool foo3()
          {
              return false && true;
          }
          bool foo4()
          {
              return false && false;
          }
          bool foo5()
          {
              return true || true;
          }
          bool foo6()
          {
              return true || false;
          }
          bool foo7()
          {
              return false || true;
          }
          bool foo8()
          {
              return false || false;
          }
      `);
    checkBool(program, callFunction(program, 'foo', [], []), true);
    checkBool(program, callFunction(program, 'foo2', [], []), false);
    checkBool(program, callFunction(program, 'foo3', [], []), false);
    checkBool(program, callFunction(program, 'foo4', [], []), false);
    checkBool(program, callFunction(program, 'foo5', [], []), true);
    checkBool(program, callFunction(program, 'foo6', [], []), true);
    checkBool(program, callFunction(program, 'foo7', [], []), true);
    checkBool(program, callFunction(program, 'foo8', [], []), false);
  });

  tests.set('typedefArray', () => {
    let program = doPrep(`
          typedef ArrayTypedef = int[2];
          int foo()
          {
              ArrayTypedef arrayTypedef;
              return arrayTypedef[0];
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), 0);
  });

  tests.set('shaderTypes', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  float4 x;
              }
              vertex Foo bar()
              {
                  Foo result;
                  result.x = float4();
                  return result;
              }
              Foo foo() {
                  return bar();
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              vertex float bar()
              {
                  return 4.;
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  float4 x;
              }
              vertex Foo bar(device Foo* x)
              {
                  return Foo();
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              struct Boo {
                  float4 x;
              }
              struct Foo {
                  float4 x;
                  device Boo* y;
              }
              vertex Foo bar()
              {
                  return Foo();
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  float4 x;
              }
              struct Boo {
                  device Foo* y;
              }
              vertex Foo bar(Boo b)
              {
                  return Foo();
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  float4 x;
              }
              vertex Foo bar(device Foo* x)
              {
                  return Foo();
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  float4 x;
              }
              fragment Foo bar(Foo foo)
              {
                  return Foo();
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  float4 x;
              }
              fragment Foo bar(device Foo* stageIn)
              {
                  return Foo();
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              struct Boo {
                  float4 x;
              }
              struct Foo {
                  float4 x;
                  device Boo* y;
              }
              fragment Boo bar(Foo stageIn)
              {
                  return boo();
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              struct Boo {
                  float4 x;
              }
              struct Foo {
                  float4 x;
                  device Boo* y;
              }
              fragment Foo bar(Boo stageIn)
              {
                  return Foo();
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('builtinVectors', () => {
    let program = doPrep(`
          int foo()
          {
              int2 a = int2(3, 4);
              return a[0];
          }
          int foo2()
          {
              int2 a = int2(3, 4);
              int3 b = int3(a, 5);
              return b[1];
          }
          int foo3()
          {
              int3 a = int3(3, 4, 5);
              int4 b = int4(6, a);
              return b[1];
          }
          int foo4()
          {
              int2 a = int2(3, 4);
              int2 b = int2(5, 6);
              int4 c = int4(a, b);
              return c[2];
          }
          bool foo5()
          {
              int4 a = int4(3, 4, 5, 6);
              int2 b = int2(4, 5);
              int4 c = int4(3, b, 6);
              return a == c;
          }
          bool foo6()
          {
              int2 a = int2(4, 5);
              int3 b = int3(3, a);
              int3 c = int3(3, 4, 6);
              return b == c;
          }
          uint foou()
          {
              uint2 a = uint2(3, 4);
              return a[0];
          }
          uint foou2()
          {
              uint2 a = uint2(3, 4);
              uint3 b = uint3(a, 5);
              return b[1];
          }
          uint foou3()
          {
              uint3 a = uint3(3, 4, 5);
              uint4 b = uint4(6, a);
              return b[1];
          }
          uint foou4()
          {
              uint2 a = uint2(3, 4);
              uint2 b = uint2(5, 6);
              uint4 c = uint4(a, b);
              return c[2];
          }
          bool foou5()
          {
              uint4 a = uint4(3, 4, 5, 6);
              uint2 b = uint2(4, 5);
              uint4 c = uint4(3, b, 6);
              return a == c;
          }
          bool foou6()
          {
              uint2 a = uint2(4, 5);
              uint3 b = uint3(3, a);
              uint3 c = uint3(3, 4, 6);
              return b == c;
          }
          float foof()
          {
              float2 a = float2(3., 4.);
              return a[0];
          }
          float foof2()
          {
              float2 a = float2(3., 4.);
              float3 b = float3(a, 5.);
              return b[1];
          }
          float foof3()
          {
              float3 a = float3(3., 4., 5.);
              float4 b = float4(6., a);
              return b[1];
          }
          float foof4()
          {
              float2 a = float2(3., 4.);
              float2 b = float2(5., 6.);
              float4 c = float4(a, b);
              return c[2];
          }
          bool foof5()
          {
              float4 a = float4(3., 4., 5., 6.);
              float2 b = float2(4., 5.);
              float4 c = float4(3., b, 6.);
              return a == c;
          }
          bool foof6()
          {
              float2 a = float2(4., 5.);
              float3 b = float3(3., a);
              float3 c = float3(3., 4., 6.);
              return b == c;
          }
          double food()
          {
              double2 a = double2(3., 4.);
              return a[0];
          }
          double food2()
          {
              double2 a = double2(3., 4.);
              double3 b = double3(a, 5.);
              return b[1];
          }
          double food3()
          {
              double3 a = double3(3., 4., 5.);
              double4 b = double4(6., a);
              return b[1];
          }
          double food4()
          {
              double2 a = double2(3., 4.);
              double2 b = double2(5., 6.);
              double4 c = double4(a, b);
              return c[2];
          }
          bool food5()
          {
              double4 a = double4(3., 4., 5., 6.);
              double2 b = double2(4., 5.);
              double4 c = double4(3., b, 6.);
              return a == c;
          }
          bool food6()
          {
              double2 a = double2(4., 5.);
              double3 b = double3(3., a);
              double3 c = double3(3., 4., 6.);
              return b == c;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_3);
    checkInt(program, callFunction(program, 'foo2', [], []), WSL_4);
    checkInt(program, callFunction(program, 'foo3', [], []), WSL_3);
    checkInt(program, callFunction(program, 'foo4', [], []), WSL_5);
    checkBool(program, callFunction(program, 'foo5', [], []), true);
    checkBool(program, callFunction(program, 'foo6', [], []), false);
    checkUint(program, callFunction(program, 'foou', [], []), WSL_3);
    checkUint(program, callFunction(program, 'foou2', [], []), WSL_4);
    checkUint(program, callFunction(program, 'foou3', [], []), WSL_3);
    checkUint(program, callFunction(program, 'foou4', [], []), WSL_5);
    checkBool(program, callFunction(program, 'foou5', [], []), true);
    checkBool(program, callFunction(program, 'foou6', [], []), false);
    checkFloat(program, callFunction(program, 'foof', [], []), WSL_3);
    checkFloat(program, callFunction(program, 'foof2', [], []), WSL_4);
    checkFloat(program, callFunction(program, 'foof3', [], []), WSL_3);
    checkFloat(program, callFunction(program, 'foof4', [], []), WSL_5);
    checkBool(program, callFunction(program, 'foof5', [], []), true);
    checkBool(program, callFunction(program, 'foof6', [], []), false);
    checkDouble(program, callFunction(program, 'food', [], []), WSL_3);
    checkDouble(program, callFunction(program, 'food2', [], []), WSL_4);
    checkDouble(program, callFunction(program, 'food3', [], []), WSL_3);
    checkDouble(program, callFunction(program, 'food4', [], []), WSL_5);
    checkBool(program, callFunction(program, 'food5', [], []), true);
    checkBool(program, callFunction(program, 'food6', [], []), false);
  });

  tests.set('instantiateStructInStruct', () => {
    let program = doPrep(`
          struct Bar<T> {
              T x;
          }
          struct Foo {
              Bar<int> x;
          }
          int foo()
          {
              Foo x;
              x.x.x = 42;
              x.x.x++;
              return x.x.x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_43);
  });

  tests.set('instantiateStructInStructWithInt2', () => {
    let program = doPrep(`
          struct Foo {
              int2 x;
          }
          int foo()
          {
              Foo x;
              x.x.x = 42;
              x.x.x++;
              return x.x.x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_43);
  });

  tests.set('simpleEnum', () => {
    let program = doPrep(`
          enum Foo {
              War,
              Famine,
              Pestilence,
              Death
          }
          Foo war()
          {
              return Foo.War;
          }
          Foo famine()
          {
              return Foo.Famine;
          }
          Foo pestilence()
          {
              return Foo.Pestilence;
          }
          Foo death()
          {
              return Foo.Death;
          }
          bool equals(Foo a, Foo b)
          {
              return a == b;
          }
          bool notEquals(Foo a, Foo b)
          {
              return a != b;
          }
          bool testSimpleEqual()
          {
              return equals(Foo.War, Foo.War);
          }
          bool testAnotherEqual()
          {
              return equals(Foo.Pestilence, Foo.Pestilence);
          }
          bool testNotEqual()
          {
              return equals(Foo.Famine, Foo.Death);
          }
          bool testSimpleNotEqual()
          {
              return notEquals(Foo.War, Foo.War);
          }
          bool testAnotherNotEqual()
          {
              return notEquals(Foo.Pestilence, Foo.Pestilence);
          }
          bool testNotNotEqual()
          {
              return notEquals(Foo.Famine, Foo.Death);
          }
          int intWar()
          {
              return int(war());
          }
          int intFamine()
          {
              return int(famine());
          }
          int intPestilence()
          {
              return int(pestilence());
          }
          int intDeath()
          {
              return int(death());
          }
          int warValue()
          {
              return war().value;
          }
          int famineValue()
          {
              return famine().value;
          }
          int pestilenceValue()
          {
              return pestilence().value;
          }
          int deathValue()
          {
              return death().value;
          }
          int warValueLiteral()
          {
              return Foo.War.value;
          }
          int famineValueLiteral()
          {
              return Foo.Famine.value;
          }
          int pestilenceValueLiteral()
          {
              return Foo.Pestilence.value;
          }
          int deathValueLiteral()
          {
              return Foo.Death.value;
          }
          Foo intWarBackwards()
          {
              return Foo(intWar());
          }
          Foo intFamineBackwards()
          {
              return Foo(intFamine());
          }
          Foo intPestilenceBackwards()
          {
              return Foo(intPestilence());
          }
          Foo intDeathBackwards()
          {
              return Foo(intDeath());
          }
      `);
    checkEnum(program, callFunction(program, 'war', [], []), 0);
    checkEnum(program, callFunction(program, 'famine', [], []), 1);
    checkEnum(program, callFunction(program, 'pestilence', [], []), WSL_2);
    checkEnum(program, callFunction(program, 'death', [], []), WSL_3);
    checkBool(program, callFunction(program, 'testSimpleEqual', [], []), true);
    checkBool(program, callFunction(program, 'testAnotherEqual', [], []), true);
    checkBool(program, callFunction(program, 'testNotEqual', [], []), false);
    checkBool(program, callFunction(program, 'testSimpleNotEqual', [], []), false);
    checkBool(program, callFunction(program, 'testAnotherNotEqual', [], []), false);
    checkBool(program, callFunction(program, 'testNotNotEqual', [], []), true);
    checkInt(program, callFunction(program, 'intWar', [], []), 0);
    checkInt(program, callFunction(program, 'intFamine', [], []), 1);
    checkInt(program, callFunction(program, 'intPestilence', [], []), WSL_2);
    checkInt(program, callFunction(program, 'intDeath', [], []), WSL_3);
    checkInt(program, callFunction(program, 'warValue', [], []), 0);
    checkInt(program, callFunction(program, 'famineValue', [], []), 1);
    checkInt(program, callFunction(program, 'pestilenceValue', [], []), WSL_2);
    checkInt(program, callFunction(program, 'deathValue', [], []), WSL_3);
    checkInt(program, callFunction(program, 'warValueLiteral', [], []), 0);
    checkInt(program, callFunction(program, 'famineValueLiteral', [], []), 1);
    checkInt(program, callFunction(program, 'pestilenceValueLiteral', [], []), WSL_2);
    checkInt(program, callFunction(program, 'deathValueLiteral', [], []), WSL_3);
    checkEnum(program, callFunction(program, 'intWarBackwards', [], []), 0);
    checkEnum(program, callFunction(program, 'intFamineBackwards', [], []), 1);
    checkEnum(program, callFunction(program, 'intPestilenceBackwards', [], []), WSL_2);
    checkEnum(program, callFunction(program, 'intDeathBackwards', [], []), WSL_3);
  });

  tests.set('enumWithManualValues', () => {
    let program = doPrep(`
          enum Foo {
              War = 72,
              Famine = 0,
              Pestilence = 23,
              Death = -42
          }
          Foo war()
          {
              return Foo.War;
          }
          Foo famine()
          {
              return Foo.Famine;
          }
          Foo pestilence()
          {
              return Foo.Pestilence;
          }
          Foo death()
          {
              return Foo.Death;
          }
      `);
    checkEnum(program, callFunction(program, 'war', [], []), WSL_72);
    checkEnum(program, callFunction(program, 'famine', [], []), 0);
    checkEnum(program, callFunction(program, 'pestilence', [], []), WSL_23);
    checkEnum(program, callFunction(program, 'death', [], []), WSLNEGATIVE_42);
  });

  tests.set('enumWithoutZero', () => {
    checkFail(
      () =>
        doPrep(`
              enum Foo {
                  War = 72,
                  Famine = 64,
                  Pestilence = 23,
                  Death = -42
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('enumDuplicates', () => {
    checkFail(
      () =>
        doPrep(`
              enum Foo {
                  War = -42,
                  Famine = 0,
                  Pestilence = 23,
                  Death = -42
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('enumWithSomeManualValues', () => {
    let program = doPrep(`
          enum Foo {
              War = 72,
              Famine,
              Pestilence = 0,
              Death
          }
          Foo war()
          {
              return Foo.War;
          }
          Foo famine()
          {
              return Foo.Famine;
          }
          Foo pestilence()
          {
              return Foo.Pestilence;
          }
          Foo death()
          {
              return Foo.Death;
          }
      `);
    checkEnum(program, callFunction(program, 'war', [], []), WSL_72);
    checkEnum(program, callFunction(program, 'famine', [], []), WSL_73);
    checkEnum(program, callFunction(program, 'pestilence', [], []), 0);
    checkEnum(program, callFunction(program, 'death', [], []), 1);
  });

  tests.set('enumConstexprGenericFunction', () => {
    let program = doPrep(`
          enum Axis { X, Y }
          int foo<Axis axis>() { return int(axis); }
          int testX() { return foo<Axis.X>(); }
          int testY() { return foo<Axis.Y>(); }
      `);
    checkInt(program, callFunction(program, 'testX', [], []), 0);
    checkInt(program, callFunction(program, 'testY', [], []), 1);
  });

  tests.set('enumConstexprGenericStruct', () => {
    let program = doPrep(`
          enum Axis { X, Y }
          struct Foo<Axis axis> { }
          int foo<Axis axis>(Foo<axis>) { return int(axis); }
          int testX()
          {
              Foo<Axis.X> f;
              return foo(f);
          }
          int testY()
          {
              Foo<Axis.Y> f;
              return foo(f);
          }
      `);
    checkInt(program, callFunction(program, 'testX', [], []), 0);
    checkInt(program, callFunction(program, 'testY', [], []), 1);
  });

  tests.set('trap', () => {
    let program = doPrep(`
          int foo()
          {
              trap;
          }
          int foo2(int x)
          {
              if (x == 3)
                  trap;
              return 4;
          }
          struct Bar {
              int3 x;
              float y;
          }
          Bar foo3()
          {
              trap;
          }
      `);
    checkFail(
      () => callFunction(program, 'foo', [], []),
      (e) => e instanceof WTrapError
    );
    checkInt(program, callFunction(program, 'foo2', [], [makeInt(program, 1)]), WSL_4);
    checkFail(
      () => callFunction(program, 'foo2', [], [makeInt(program, WSL_3)]),
      (e) => e instanceof WTrapError
    );
    checkFail(
      () => callFunction(program, 'foo3', [], []),
      (e) => e instanceof WTrapError
    );
  });

  tests.set('swizzle', () => {
    let program = doPrep(`
          float foo() {
              float4 bar = float4(3., 4., 5., 6.);
              float3 baz = bar.zzx;
              return baz.z;
          }
          float foo2() {
              float4 bar = float4(3., 4., 5., 6.);
              float3 baz = bar.wyz;
              return baz.x;
          }
          float foo3() {
              float3 bar = float3(3., 4., 5.);
              float2 baz = bar.yz;
              float4 quix = baz.yyxx;
              return quix.z;
          }
      `);
    checkFloat(program, callFunction(program, 'foo', [], []), WSL_3);
    checkFloat(program, callFunction(program, 'foo2', [], []), WSL_6);
    checkFloat(program, callFunction(program, 'foo3', [], []), WSL_4);
  });

  tests.set('enumWithExplicitIntBase', () => {
    let program = doPrep(`
          enum Foo : int {
              War,
              Famine,
              Pestilence,
              Death
          }
          Foo war()
          {
              return Foo.War;
          }
          Foo famine()
          {
              return Foo.Famine;
          }
          Foo pestilence()
          {
              return Foo.Pestilence;
          }
          Foo death()
          {
              return Foo.Death;
          }
          bool equals(Foo a, Foo b)
          {
              return a == b;
          }
          bool notEquals(Foo a, Foo b)
          {
              return a != b;
          }
          bool testSimpleEqual()
          {
              return equals(Foo.War, Foo.War);
          }
          bool testAnotherEqual()
          {
              return equals(Foo.Pestilence, Foo.Pestilence);
          }
          bool testNotEqual()
          {
              return equals(Foo.Famine, Foo.Death);
          }
          bool testSimpleNotEqual()
          {
              return notEquals(Foo.War, Foo.War);
          }
          bool testAnotherNotEqual()
          {
              return notEquals(Foo.Pestilence, Foo.Pestilence);
          }
          bool testNotNotEqual()
          {
              return notEquals(Foo.Famine, Foo.Death);
          }
          int intWar()
          {
              return int(war());
          }
          int intFamine()
          {
              return int(famine());
          }
          int intPestilence()
          {
              return int(pestilence());
          }
          int intDeath()
          {
              return int(death());
          }
          int warValue()
          {
              return war().value;
          }
          int famineValue()
          {
              return famine().value;
          }
          int pestilenceValue()
          {
              return pestilence().value;
          }
          int deathValue()
          {
              return death().value;
          }
          int warValueLiteral()
          {
              return Foo.War.value;
          }
          int famineValueLiteral()
          {
              return Foo.Famine.value;
          }
          int pestilenceValueLiteral()
          {
              return Foo.Pestilence.value;
          }
          int deathValueLiteral()
          {
              return Foo.Death.value;
          }
          Foo intWarBackwards()
          {
              return Foo(intWar());
          }
          Foo intFamineBackwards()
          {
              return Foo(intFamine());
          }
          Foo intPestilenceBackwards()
          {
              return Foo(intPestilence());
          }
          Foo intDeathBackwards()
          {
              return Foo(intDeath());
          }
      `);
    checkEnum(program, callFunction(program, 'war', [], []), 0);
    checkEnum(program, callFunction(program, 'famine', [], []), 1);
    checkEnum(program, callFunction(program, 'pestilence', [], []), WSL_2);
    checkEnum(program, callFunction(program, 'death', [], []), WSL_3);
    checkBool(program, callFunction(program, 'testSimpleEqual', [], []), true);
    checkBool(program, callFunction(program, 'testAnotherEqual', [], []), true);
    checkBool(program, callFunction(program, 'testNotEqual', [], []), false);
    checkBool(program, callFunction(program, 'testSimpleNotEqual', [], []), false);
    checkBool(program, callFunction(program, 'testAnotherNotEqual', [], []), false);
    checkBool(program, callFunction(program, 'testNotNotEqual', [], []), true);
    checkInt(program, callFunction(program, 'intWar', [], []), 0);
    checkInt(program, callFunction(program, 'intFamine', [], []), 1);
    checkInt(program, callFunction(program, 'intPestilence', [], []), WSL_2);
    checkInt(program, callFunction(program, 'intDeath', [], []), WSL_3);
    checkInt(program, callFunction(program, 'warValue', [], []), 0);
    checkInt(program, callFunction(program, 'famineValue', [], []), 1);
    checkInt(program, callFunction(program, 'pestilenceValue', [], []), WSL_2);
    checkInt(program, callFunction(program, 'deathValue', [], []), WSL_3);
    checkInt(program, callFunction(program, 'warValueLiteral', [], []), 0);
    checkInt(program, callFunction(program, 'famineValueLiteral', [], []), 1);
    checkInt(program, callFunction(program, 'pestilenceValueLiteral', [], []), WSL_2);
    checkInt(program, callFunction(program, 'deathValueLiteral', [], []), WSL_3);
    checkEnum(program, callFunction(program, 'intWarBackwards', [], []), 0);
    checkEnum(program, callFunction(program, 'intFamineBackwards', [], []), 1);
    checkEnum(program, callFunction(program, 'intPestilenceBackwards', [], []), WSL_2);
    checkEnum(program, callFunction(program, 'intDeathBackwards', [], []), WSL_3);
  });

  tests.set('enumWithUintBase', () => {
    let program = doPrep(`
          enum Foo : uint {
              War,
              Famine,
              Pestilence,
              Death
          }
          Foo war()
          {
              return Foo.War;
          }
          Foo famine()
          {
              return Foo.Famine;
          }
          Foo pestilence()
          {
              return Foo.Pestilence;
          }
          Foo death()
          {
              return Foo.Death;
          }
          bool equals(Foo a, Foo b)
          {
              return a == b;
          }
          bool notEquals(Foo a, Foo b)
          {
              return a != b;
          }
          bool testSimpleEqual()
          {
              return equals(Foo.War, Foo.War);
          }
          bool testAnotherEqual()
          {
              return equals(Foo.Pestilence, Foo.Pestilence);
          }
          bool testNotEqual()
          {
              return equals(Foo.Famine, Foo.Death);
          }
          bool testSimpleNotEqual()
          {
              return notEquals(Foo.War, Foo.War);
          }
          bool testAnotherNotEqual()
          {
              return notEquals(Foo.Pestilence, Foo.Pestilence);
          }
          bool testNotNotEqual()
          {
              return notEquals(Foo.Famine, Foo.Death);
          }
          uint uintWar()
          {
              return uint(war());
          }
          uint uintFamine()
          {
              return uint(famine());
          }
          uint uintPestilence()
          {
              return uint(pestilence());
          }
          uint uintDeath()
          {
              return uint(death());
          }
          uint warValue()
          {
              return war().value;
          }
          uint famineValue()
          {
              return famine().value;
          }
          uint pestilenceValue()
          {
              return pestilence().value;
          }
          uint deathValue()
          {
              return death().value;
          }
          uint warValueLiteral()
          {
              return Foo.War.value;
          }
          uint famineValueLiteral()
          {
              return Foo.Famine.value;
          }
          uint pestilenceValueLiteral()
          {
              return Foo.Pestilence.value;
          }
          uint deathValueLiteral()
          {
              return Foo.Death.value;
          }
          Foo uintWarBackwards()
          {
              return Foo(uintWar());
          }
          Foo uintFamineBackwards()
          {
              return Foo(uintFamine());
          }
          Foo uintPestilenceBackwards()
          {
              return Foo(uintPestilence());
          }
          Foo uintDeathBackwards()
          {
              return Foo(uintDeath());
          }
      `);
    checkEnum(program, callFunction(program, 'war', [], []), 0);
    checkEnum(program, callFunction(program, 'famine', [], []), 1);
    checkEnum(program, callFunction(program, 'pestilence', [], []), WSL_2);
    checkEnum(program, callFunction(program, 'death', [], []), WSL_3);
    checkBool(program, callFunction(program, 'testSimpleEqual', [], []), true);
    checkBool(program, callFunction(program, 'testAnotherEqual', [], []), true);
    checkBool(program, callFunction(program, 'testNotEqual', [], []), false);
    checkBool(program, callFunction(program, 'testSimpleNotEqual', [], []), false);
    checkBool(program, callFunction(program, 'testAnotherNotEqual', [], []), false);
    checkBool(program, callFunction(program, 'testNotNotEqual', [], []), true);
    checkUint(program, callFunction(program, 'uintWar', [], []), 0);
    checkUint(program, callFunction(program, 'uintFamine', [], []), 1);
    checkUint(program, callFunction(program, 'uintPestilence', [], []), WSL_2);
    checkUint(program, callFunction(program, 'uintDeath', [], []), WSL_3);
    checkUint(program, callFunction(program, 'warValue', [], []), 0);
    checkUint(program, callFunction(program, 'famineValue', [], []), 1);
    checkUint(program, callFunction(program, 'pestilenceValue', [], []), WSL_2);
    checkUint(program, callFunction(program, 'deathValue', [], []), WSL_3);
    checkUint(program, callFunction(program, 'warValueLiteral', [], []), 0);
    checkUint(program, callFunction(program, 'famineValueLiteral', [], []), 1);
    checkUint(program, callFunction(program, 'pestilenceValueLiteral', [], []), WSL_2);
    checkUint(program, callFunction(program, 'deathValueLiteral', [], []), WSL_3);
    checkEnum(program, callFunction(program, 'uintWarBackwards', [], []), 0);
    checkEnum(program, callFunction(program, 'uintFamineBackwards', [], []), 1);
    checkEnum(program, callFunction(program, 'uintPestilenceBackwards', [], []), WSL_2);
    checkEnum(program, callFunction(program, 'uintDeathBackwards', [], []), WSL_3);
  });

  tests.set('enumFloatBase', () => {
    checkFail(
      () =>
        doPrep(`
              enum Foo : float {
                  Bar
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('enumPtrBase', () => {
    checkFail(
      () =>
        doPrep(`
              enum Foo : thread int* {
                  Bar
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('enumArrayRefBase', () => {
    checkFail(
      () =>
        doPrep(`
              enum Foo : thread int[] {
                  Bar
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('emptyStruct', () => {
    let program = doPrep(`
          struct Thingy { }
          int foo()
          {
              Thingy thingy;
              return 46;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_46);
  });

  tests.set('enumStructBase', () => {
    checkFail(
      () =>
        doPrep(`
              struct Thingy { }
              enum Foo : Thingy {
                  Bar
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('enumNoMembers', () => {
    checkFail(
      () =>
        doPrep(`
              enum Foo { }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('simpleSwitch', () => {
    let program = doPrep(`
          int foo(int x)
          {
              switch (x) {
              case 767:
                  return 27;
              case 69:
                  return 7624;
              default:
                  return 49;
              }
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_767)]), WSL_27);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_69)]), WSL_7624);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 0)]), WSL_49);
  });

  tests.set('exhaustiveUint8Switch', () => {
    let text = 'double foo(uint8 x) { switch (uint8(x)) {';
    for (let i = 0; i <= WSL_0XFF; ++i) {
      text += `case ${i}: return ${i * WSLDECIMAL_1};`;
    }
    text += '} }';
    let program = doPrep(text);
    for (let i = 0; i < WSL_0XFF; ++i) {
      checkDouble(program, callFunction(program, 'foo', [], [makeUint8(program, i)]), i * WSLDECIMAL_1);
    }
  });

  tests.set('notQuiteExhaustiveUint8Switch', () => {
    let text = 'double foo(uint8 x) { switch (uint8(x)) {';
    for (let i = 0; i <= WSL_0XFE; ++i) {
      text += `case ${i}: return ${i * WSLDECIMAL_1};`;
    }
    text += '} }';
    checkFail(
      () => doPrep(text),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('notQuiteExhaustiveUint8SwitchWithDefault', () => {
    let text = 'double foo(uint8 x) { switch (uint8(x)) {';
    for (let i = 0; i <= WSL_0XFE; ++i) {
      text += `case ${i}: return ${i * WSLDECIMAL_1};`;
    }
    text += `default: return ${WSL_0XFF * WSLDECIMAL_1};`;
    text += '} }';
    let program = doPrep(text);
    for (let i = 0; i < WSL_0XFF; ++i) {
      checkDouble(program, callFunction(program, 'foo', [], [makeUint8(program, i)]), i * WSLDECIMAL_1);
    }
  });

  tests.set('switchFallThrough', () => {
    // FIXME: This might become an error in future versions.
    // https://bugs.webkit.org/show_bug.cgi?id=177172
    let program = doPrep(`
          int foo(int x)
          {
              int result = 0;
              switch (x) {
              case 767:
                  result += 27;
              case 69:
                  result += 7624;
              default:
                  result += 49;
              }
              return result;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_767)]), WSL_27 + WSL_7624 + WSL_49);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_69)]), WSL_7624 + WSL_49);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 0)]), WSL_49);
  });

  tests.set('switchBreak', () => {
    let program = doPrep(`
          int foo(int x)
          {
              int result = 0;
              switch (x) {
              case 767:
                  result += 27;
                  break;
              case 69:
                  result += 7624;
                  break;
              default:
                  result += 49;
                  break;
              }
              return result;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_767)]), WSL_27);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, WSL_69)]), WSL_7624);
    checkInt(program, callFunction(program, 'foo', [], [makeInt(program, 0)]), WSL_49);
  });

  tests.set('enumSwitchBreakExhaustive', () => {
    let program = doPrep(`
          enum Foo {
              A, B, C
          }
          int foo(Foo x)
          {
              int result = 0;
              switch (x) {
              case Foo.A:
                  result += 27;
                  break;
              case Foo.B:
                  result += 7624;
                  break;
              case Foo.C:
                  result += 49;
                  break;
              }
              return result;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeEnum(program, 'Foo', 'A')]), WSL_27);
    checkInt(program, callFunction(program, 'foo', [], [makeEnum(program, 'Foo', 'B')]), WSL_7624);
    checkInt(program, callFunction(program, 'foo', [], [makeEnum(program, 'Foo', 'C')]), WSL_49);
  });

  tests.set('enumSwitchBreakNotQuiteExhaustive', () => {
    checkFail(
      () =>
        doPrep(`
              enum Foo {
                  A, B, C, D
              }
              int foo(Foo x)
              {
                  int result = 0;
                  switch (x) {
                  case Foo.A:
                      result += 27;
                      break;
                  case Foo.B:
                      result += 7624;
                      break;
                  case Foo.C:
                      result += 49;
                      break;
                  }
                  return result;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('enumSwitchBreakNotQuiteExhaustiveWithDefault', () => {
    let program = doPrep(`
          enum Foo {
              A, B, C
          }
          int foo(Foo x)
          {
              int result = 0;
              switch (x) {
              case Foo.A:
                  result += 27;
                  break;
              case Foo.B:
                  result += 7624;
                  break;
              default:
                  result += 49;
                  break;
              }
              return result;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], [makeEnum(program, 'Foo', 'A')]), WSL_27);
    checkInt(program, callFunction(program, 'foo', [], [makeEnum(program, 'Foo', 'B')]), WSL_7624);
    checkInt(program, callFunction(program, 'foo', [], [makeEnum(program, 'Foo', 'C')]), WSL_49);
  });

  tests.set('simpleRecursiveStruct', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  Foo foo;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('mutuallyRecursiveStruct', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  Bar bar;
              }
              struct Bar {
                  Foo foo;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('mutuallyRecursiveStructWithPointersBroken', () => {
    let program = doPrep(`
          struct Foo {
              thread Bar* bar;
              int foo;
          }
          struct Bar {
              thread Foo* foo;
              int bar;
          }
          int foo()
          {
              Foo foo;
              Bar bar;
              foo.foo = 564;
              bar.bar = 53;
              return foo.bar->bar - bar.foo->foo;
          }
      `);
    checkFail(
      () => checkInt(program, callFunction(program, 'foo', [], []), WSLNEGATIVE_511),
      (e) => e instanceof WTrapError
    );
  });

  tests.set('mutuallyRecursiveStructWithPointers', () => {
    let program = doPrep(`
          struct Foo {
              thread Bar* bar;
              int foo;
          }
          struct Bar {
              thread Foo* foo;
              int bar;
          }
          int foo()
          {
              Foo foo;
              Bar bar;
              foo.bar = &bar;
              bar.foo = &foo;
              foo.foo = 564;
              bar.bar = 53;
              return foo.bar->bar - bar.foo->foo;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSLNEGATIVE_511);
  });

  tests.set('linkedList', () => {
    let program = doPrep(`
          struct Node {
              thread Node* next;
              int value;
          }
          int foo()
          {
              Node x, y, z;
              x.next = &y;
              y.next = &z;
              x.value = 1;
              y.value = 2;
              z.value = 3;
              return x.next->next->value;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_3);
  });

  tests.set('pointerToPointer', () => {
    let program = doPrep(`
          int foo()
          {
              int x;
              thread int* p = &x;
              thread int** pp = &p;
              int*thread*thread qq = pp;
              int result = 0;
              x = 42;
              *p = 76;
              result += x;
              **pp = 39;
              result += x;
              **qq = 83;
              result += x;
              return result;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_76 + WSL_39 + WSL_83);
  });

  tests.set('arrayRefToArrayRef', () => {
    let program = doPrep(`
          int foo()
          {
              int x;
              thread int[] p = @x;
              thread int[][] pp = @p;
              int[]thread[]thread qq = pp;
              int result = 0;
              x = 42;
              p[0] = 76;
              result += x;
              pp[0][0] = 39;
              result += x;
              qq[0][0] = 83;
              result += x;
              return result;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_76 + WSL_39 + WSL_83);
  });

  tests.set('pointerGetter', () => {
    checkFail(
      () =>
        doPrep(`
              int operator.foo(device int*)
              {
                  return 543;
              }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator.foo(thread int*)
              {
                  return 543;
              }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator.foo(threadgroup int*)
              {
                  return 543;
              }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator.foo(constant int*)
              {
                  return 543;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('loneSetter', () => {
    checkFail(
      () =>
        doPrep(`
              int operator.foo=(int, int)
              {
                  return 543;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('setterWithMismatchedType', () => {
    checkFail(
      () =>
        doPrep(`
              double operator.foo(int)
              {
                  return 5.43;
              }
              int operator.foo=(int, int)
              {
                  return 543;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('setterWithMatchedType', () => {
    doPrep(`
          int operator.foo(int)
          {
              return 5;
          }
          int operator.foo=(int, int)
          {
              return 543;
          }
      `);
  });

  tests.set('operatorWithUninferrableTypeVariable', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  int x;
              }
              Foo operator+<T>(Foo a, Foo b)
              {
                  Foo result;
                  result.x = a.x + b.x;
                  return result;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('operatorWithoutUninferrableTypeVariable', () => {
    let program = doPrep(`
          struct Foo {
              int x;
          }
          Foo operator+(Foo a, Foo b)
          {
              Foo result;
              result.x = a.x + b.x;
              return result;
          }
          int foo()
          {
              Foo a;
              a.x = 645;
              Foo b;
              b.x = -35;
              return (a + b).x;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_645 - WSL_35);
  });

  tests.set('operatorCastWithUninferrableTypeVariable', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  int x;
              }
              operator<T> Foo(int x)
              {
                  Foo result;
                  result.x = x;
                  return result;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('operatorCastWithTypeVariableInferredFromReturnType', () => {
    let program = doPrep(`
          struct Foo {
              int x;
          }
          protocol Barable {
              void bar(thread Barable*, int);
          }
          void bar(thread double* result, int value)
          {
              *result = double(value);
          }
          operator<T:Barable> T(Foo foo)
          {
              T result;
              bar(&result, foo.x);
              return result;
          }
          int foo()
          {
              Foo foo;
              foo.x = 75;
              double x = double(foo);
              return int(x * 1.5);
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_112);
  });

  tests.set('incWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator++() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator++(int, int) { return 76; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('decWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator--() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator--(int, int) { return 76; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('incWrongTypes', () => {
    checkFail(
      () =>
        doPrep(`
              int operator++(double) { return 32; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('decWrongTypes', () => {
    checkFail(
      () =>
        doPrep(`
              int operator--(double) { return 32; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('plusWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator+() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator+(int, int, int) { return 76; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('minusWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator-() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator-(int, int, int) { return 76; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('timesWrongArgumentLength', () => {
    checkFail(
      (): Program =>
        doPrep(`
              int operator*() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator*(int) { return 534; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator*(int, int, int) { return 76; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('divideWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator/() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator/(int) { return 534; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator/(int, int, int) { return 76; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('moduloWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator%() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator%(int) { return 534; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator%(int, int, int) { return 76; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('bitAndWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator&() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator&(int) { return 534; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator&(int, int, int) { return 76; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('bitOrWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator|() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator|(int) { return 534; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator|(int, int, int) { return 76; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('bitXorWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator^() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator^(int) { return 534; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator^(int, int, int) { return 76; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('lShiftWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator<<() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator<<(int) { return 534; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator<<(int, int, int) { return 76; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('rShiftWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator>>() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator>>(int) { return 534; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator>>(int, int, int) { return 76; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('bitNotWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator~() { return 32; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator~(int, int) { return 534; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('equalsWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              bool operator==() { return true; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              bool operator==(int) { return true; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              bool operator==(int, int, int) { return true; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('lessThanWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              bool operator<() { return true; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              bool operator<(int) { return true; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              bool operator<(int, int, int) { return true; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('lessEqualWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              bool operator<=() { return true; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              bool operator<=(int) { return true; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              bool operator<=(int, int, int) { return true; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('greaterWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              bool operator>() { return true; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              bool operator>(int) { return true; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              bool operator>(int, int, int) { return true; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('greaterEqualWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              bool operator>=() { return true; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              bool operator>=(int) { return true; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              bool operator>=(int, int, int) { return true; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('equalsWrongReturnType', () => {
    checkFail(
      () =>
        doPrep(`
              int operator==(int a, int b) { return a + b; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('notEqualsOverload', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo { }
              bool operator!=(Foo, Foo) { return true; }
          `),
      (e) => e instanceof WSyntaxError
    );
  });

  tests.set('lessThanWrongReturnType', () => {
    checkFail(
      () =>
        doPrep(`
              int operator<(int a, int b) { return a + b; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('lessEqualWrongReturnType', () => {
    checkFail(
      () =>
        doPrep(`
              int operator<=(int a, int b) { return a + b; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('greaterThanWrongReturnType', () => {
    checkFail(
      () =>
        doPrep(`
              int operator>(int a, int b) { return a + b; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('greaterEqualWrongReturnType', () => {
    checkFail(
      () =>
        doPrep(`
              int operator>=(int a, int b) { return a + b; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('dotOperatorWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator.foo() { return 42; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              struct Foo { }
              int operator.foo(Foo, int) { return 42; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('dotOperatorSetterWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo { }
              Foo operator.foo=() { return 42; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              struct Foo { }
              Foo operator.foo=(Foo) { return 42; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              struct Foo { }
              Foo operator.foo=(Foo, int, int) { return 42; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('loneSetterPointer', () => {
    checkFail(
      () =>
        doPrep(`
              thread int* operator.foo=(thread int* ptr, int)
              {
                  return ptr;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('setterWithNoGetterOverload', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo { }
              struct Bar { }
              int operator.foo(Foo)
              {
                  return 534;
              }
              Bar operator.foo=(Bar, int)
              {
                  return Bar();
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('setterWithNoGetterOverloadFixed', () => {
    doPrep(`
          struct Bar { }
          int operator.foo(Bar)
          {
              return 534;
          }
          Bar operator.foo=(Bar, int)
          {
              return Bar();
          }
      `);
  });

  tests.set('anderWithNothingWrong', () => {
    let program = doPrep(`
          struct Foo {
              int x;
          }
          thread int* operator&.foo(thread Foo* foo)
          {
              return &foo->x;
          }
          int foo()
          {
              Foo x;
              x.x = 13;
              return x.foo;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_13);
  });

  tests.set('anderWithWrongNumberOfArguments', () => {
    checkFail(
      () =>
        doPrep(`
              thread int* operator&.foo()
              {
                  int x;
                  return &x;
              }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  int x;
              }
              thread int* operator&.foo(thread Foo* foo, int blah)
              {
                  return &foo->x;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('anderDoesntReturnPointer', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  int x;
              }
              int operator&.foo(thread Foo* foo)
              {
                  return foo->x;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('anderDoesntTakeReference', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  int x;
              }
              thread int* operator&.foo(Foo foo)
              {
                  return &foo.x;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('anderWithArrayRef', () => {
    let program = doPrep(`
          struct Foo {
              int x;
          }
          thread int* operator&.foo(thread Foo[] foo)
          {
              return &foo[0].x;
          }
          int foo()
          {
              Foo x;
              x.x = 13;
              return (@x).foo;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_13);
  });

  tests.set('pointerIndexGetter', () => {
    checkFail(
      () =>
        doPrep(`
              int operator[](device int*, uint)
              {
                  return 543;
              }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator[](thread int*, uint)
              {
                  return 543;
              }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator[](threadgroup int*, uint)
              {
                  return 543;
              }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator[](constant int*, uint)
              {
                  return 543;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('loneIndexSetter', () => {
    checkFail(
      () =>
        doPrep(`
              int operator[]=(int, uint, int)
              {
                  return 543;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('notLoneIndexSetter', () => {
    doPrep(`
          int operator[](int, uint)
          {
              return 65;
          }
          int operator[]=(int, uint, int)
          {
              return 543;
          }
      `);
  });

  tests.set('indexSetterWithMismatchedType', () => {
    checkFail(
      () =>
        doPrep(`
              double operator[](int, uint)
              {
                  return 5.43;
              }
              int operator[]=(int, uint, int)
              {
                  return 543;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Setter and getter must agree on value type') !== -1
    );
  });

  tests.set('indexOperatorWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator[]() { return 42; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator[](int) { return 42; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator[](int, int, int) { return 42; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('indexOperatorSetterWrongArgumentLength', () => {
    checkFail(
      () =>
        doPrep(`
              int operator[]=() { return 42; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator[]=(int) { return 42; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator[]=(int, int) { return 42; }
          `),
      (e) => e instanceof WTypeError
    );

    checkFail(
      () =>
        doPrep(`
              int operator[]=(int, int, int, int) { return 42; }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('loneIndexSetterPointer', () => {
    checkFail(
      () =>
        doPrep(`
              thread int* operator[]=(thread int* ptr, uint, int)
              {
                  return ptr;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('indexSetterWithNoGetterOverload', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo { }
              struct Bar { }
              int operator[](Foo, uint)
              {
                  return 534;
              }
              Bar operator[]=(Bar, uint, int)
              {
                  return Bar();
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('indexSetterWithNoGetterOverloadFixed', () => {
    doPrep(`
          struct Bar { }
          int operator[](Bar, uint)
          {
              return 534;
          }
          Bar operator[]=(Bar, uint, int)
          {
              return Bar();
          }
      `);
  });

  tests.set('indexAnderWithNothingWrong', () => {
    let program = doPrep(`
          struct Foo {
              int x;
          }
          thread int* operator&[](thread Foo* foo, uint)
          {
              return &foo->x;
          }
          int foo()
          {
              Foo x;
              x.x = 13;
              return x[666];
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_13);
  });

  tests.set('indexAnderWithWrongNumberOfArguments', () => {
    checkFail(
      () =>
        doPrep(`
              thread int* operator&[]()
              {
                  int x;
                  return &x;
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  int x;
              }
              thread int* operator&[](thread Foo* foo)
              {
                  return &foo->x;
              }
          `),
      (e) => e instanceof WTypeError
    );
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  int x;
              }
              thread int* operator&[](thread Foo* foo, uint, uint)
              {
                  return &foo->x;
              }
          `),
      (e) => e instanceof WTypeError
    );
  });

  tests.set('indexAnderDoesntReturnPointer', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  int x;
              }
              int operator&[](thread Foo* foo, uint)
              {
                  return foo->x;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Return type of ander is not a pointer') !== -1
    );
  });

  tests.set('indexAnderDoesntTakeReference', () => {
    checkFail(
      () =>
        doPrep(`
              struct Foo {
                  int x;
              }
              thread int* operator&[](Foo foo, uint)
              {
                  return &foo.x;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Parameter to ander is not a reference') !== -1
    );
  });

  tests.set('indexAnderWithArrayRef', () => {
    let program = doPrep(`
          struct Foo {
              int x;
          }
          thread int* operator&[](thread Foo[] array, double index)
          {
              return &array[uint(index + 1)].x;
          }
          int foo()
          {
              Foo x;
              x.x = 13;
              return (@x)[double(-1)];
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_13);
  });

  tests.set('devicePtrPtr', () => {
    checkFail(
      () =>
        doPrep(`
              void foo()
              {
                  device int** p;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Illegal pointer to non-primitive type: int32* device* device') !== -1
    );
  });

  tests.set('threadgroupPtrPtr', () => {
    checkFail(
      () =>
        doPrep(`
              void foo()
              {
                  threadgroup int** p;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Illegal pointer to non-primitive type: int32* threadgroup* threadgroup') !== -1
    );
  });

  tests.set('constantPtrPtr', () => {
    checkFail(
      () =>
        doPrep(`
              void foo()
              {
                  constant int** p;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Illegal pointer to non-primitive type: int32* constant* constant') !== -1
    );
  });

  tests.set('pointerIndexGetterInProtocol', () => {
    for (let addressSpace of addressSpaces) {
      checkFail(
        () =>
          doPrep(`
                  protocol Foo {
                      int operator[](${addressSpace} Foo*, uint);
                  }
                  struct Bar { }
                  int operator[](Bar, uint) { return 42; }
              `),
        (e) => e instanceof WTypeError && e.message.indexOf('Cannot have getter for pointer type') !== -1
      );
    }
  });

  tests.set('loneIndexSetterInProtocol', () => {
    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  Foo operator[]=(Foo, uint, int);
              }
              struct Bar { }
              int operator[](Bar, uint) { return 42; }
              Bar operator[]=(Bar, uint, int) { return Bar(); }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Every setter must have a matching getter') !== -1
    );
  });

  tests.set('notLoneIndexSetterInProtocol', () => {
    doPrep(`
          protocol Foo {
              int operator[](Foo, uint);
              Foo operator[]=(Foo, uint, int);
          }
          struct Bar { }
          int operator[](Bar, uint) { return 42; }
          Bar operator[]=(Bar, uint, int) { return Bar(); }
      `);
  });

  tests.set('indexSetterWithMismatchedTypeInProtocol', () => {
    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  double operator[](Foo, uint);
                  Foo operator[]=(Foo, uint, int);
              }
              struct Bar { }
              int operator[](Bar, uint) { return 42; }
              Bar operator[]=(Bar, uint, int) { return Bar(); }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Setter and getter must agree on value type') !== -1
    );
  });

  tests.set('indexOperatorWrongArgumentLengthInProtocol', () => {
    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  int operator[]();
              }
              struct Bar { }
              int operator[](Bar, uint) { return 42; }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Protocol`s type variable (Foo) not mentioned in signature') !== -1
    );

    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  int operator[](Foo);
              }
              struct Bar { }
              int operator[](Bar, uint) { return 42; }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Incorrect number of parameters') !== -1
    );

    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  int operator[](Foo, int, int);
              }
              struct Bar { }
              int operator[](Bar, uint) { return 42; }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Incorrect number of parameters') !== -1
    );
  });

  tests.set('indexOperatorSetterWrongArgumentLengthInProtocol', () => {
    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  int operator[]=();
              }
              struct Bar { }
              int operator[](Bar, uint) { return 42; }
              Bar operator[]=(Bar, uint, int) { return Bar(); }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Protocol`s type variable (Foo) not mentioned in signature') !== -1
    );

    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  int operator[]=(Foo);
              }
              struct Bar { }
              int operator[](Bar, uint) { return 42; }
              Bar operator[]=(Bar, uint, int) { return Bar(); }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Incorrect number of parameters') !== -1
    );

    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  int operator[]=(Foo, int);
              }
              struct Bar { }
              int operator[](Bar, uint) { return 42; }
              Bar operator[]=(Bar, uint, int) { return Bar(); }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Incorrect number of parameters') !== -1
    );

    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  int operator[]=(Foo, int, int, int);
              }
              struct Bar { }
              int operator[](Bar, uint) { return 42; }
              Bar operator[]=(Bar, uint, int) { return Bar(); }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Incorrect number of parameters') !== -1
    );
  });

  tests.set('loneIndexSetterPointerInProtocol', () => {
    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  thread int* operator[]=(thread Foo* ptr, uint, int);
              }
              struct Bar { }
              int operator[](Bar, uint) { return 42; }
              Bar operator[]=(Bar, uint, int) { return Bar(); }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Cannot have setter for pointer type') !== -1
    );
  });

  tests.set('indexSetterWithNoGetterOverloadInProtocol', () => {
    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  int operator[](int, Foo);
                  Foo operator[]=(Foo, uint, int);
              }
              struct Bar { }
              int operator[](Bar, uint) { return 42; }
              Bar operator[]=(Bar, uint, int) { return Bar(); }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Did not find function named operator[]= with arguments Foo,uint32') !== -1
    );
  });

  tests.set('indexSetterWithNoGetterOverloadFixedInProtocol', () => {
    doPrep(`
          protocol Foo {
              int operator[](Foo, uint);
              Foo operator[]=(Foo, uint, int);
          }
          struct Bar { }
          int operator[](Bar, uint) { return 42; }
          Bar operator[]=(Bar, uint, int) { return Bar(); }
      `);
  });

  tests.set('indexAnderWithNothingWrongInProtocol', () => {
    let program = doPrep(`
          protocol Foo {
              thread int* operator&[](thread Foo* foo, uint);
          }
          int bar<T:Foo>(T x)
          {
              return x[42];
          }
          struct Bar { }
          thread int* operator&[](thread Bar*, uint)
          {
              int result = 1234;
              return &result;
          }
          int foo()
          {
              return bar(Bar());
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_1234);
  });

  tests.set('indexAnderWithWrongNumberOfArgumentsInProtocol', () => {
    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  thread int* operator&[]();
              }
              struct Bar { }
              thread int* operator&[](thread Bar*, uint)
              {
                  int result = 1234;
                  return &result;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Protocol`s type variable (Foo) not mentioned in signature') !== -1
    );

    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  thread int* operator&[](thread Foo* foo);
              }
              struct Bar { }
              thread int* operator&[](thread Bar*, uint)
              {
                  int result = 1234;
                  return &result;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Incorrect number of parameters for operator&[]') !== -1
    );

    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  thread int* operator&[](thread Foo* foo, uint, uint);
              }
              struct Bar { }
              thread int* operator&[](thread Bar*, uint)
              {
                  int result = 1234;
                  return &result;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Incorrect number of parameters for operator&[]') !== -1
    );
  });

  tests.set('indexAnderDoesntReturnPointerInProtocol', () => {
    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  int operator&[](thread Foo* foo, uint);
              }
              struct Bar { }
              thread int* operator&[](thread Bar*, uint)
              {
                  int result = 1234;
                  return &result;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Return type of ander is not a pointer') !== -1
    );
  });

  tests.set('indexAnderDoesntTakeReferenceInProtocol', () => {
    checkFail(
      () =>
        doPrep(`
              protocol Foo {
                  thread int* operator&[](Foo foo, uint);
              }
              struct Bar { }
              thread int* operator&[](thread Bar*, uint)
              {
                  int result = 1234;
                  return &result;
              }
          `),
      (e) => e instanceof WTypeError && e.message.indexOf('Parameter to ander is not a reference') !== -1
    );
  });

  tests.set('indexAnderWithArrayRefInProtocol', () => {
    let program = doPrep(`
          protocol Foo {
              thread int* operator&[](thread Foo[] array, double index);
          }
          int bar<T:Foo>(thread T[] x)
          {
              return x[1.5];
          }
          struct Bar { }
          thread int* operator&[](thread Bar[], double)
          {
              int result = 1234;
              return &result;
          }
          int foo()
          {
              Bar x;
              return bar(@x);
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_1234);
  });

  tests.set('andReturnedArrayRef', () => {
    let program = doPrep(`
          thread int[] getArray()
          {
              int[10] x;
              x[5] = 354;
              return @x;
          }
          int foo()
          {
              thread int* ptr = &getArray()[5];
              return *ptr;
          }
      `);
    checkInt(program, callFunction(program, 'foo', [], []), WSL_354);
  });

  return tests;
}

function doTest(): void {
  let tests = getTests();
  okToTest = true;
  if (!okToTest) {
    throw new Error('Test setup is incomplete.');
  }
  let names: string[] = [];
  for (let s of tests.keys()) {
    names.push(s);
  }
  names.sort();
  for (const name of names) {
    try {
      debugLog(`Testing: ${name}`);
      tests.get(name)!();
    } catch (error) {}
  }
}

function debugLog(str: string): void {
  //print(str);
}

export default class Benchmark {
  run(): void {
    doTest();
  }
}

declare interface ArkTools {
  timeInUs(args: number): number;
}
function currentTimestamp13(): number {
  return ArkTools.timeInUs() / 1000;
}
let startTime = currentTimestamp13();
new Benchmark().run();
let endTime = currentTimestamp13();
print(`wsl: ms = ${endTime - startTime}`);
